From c63e7d0d1ff5c45178bc4f4feb131e8edb5aa324 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 May 2022 20:14:44 -0700 Subject: [PATCH 1/7] Bump to version 0.7.9 --- .gitignore | 2 + CHANGELOG.md | 14 + README.md | 9 +- docs/404.html | 8 +- docs/Apache2/index.html | 8 +- docs/Commands/index.html | 12 +- docs/assets/_mkdocstrings.css | 20 + docs/changelog/index.html | 35 +- docs/index.html | 30 +- docs/man/index.html | 26 +- docs/objects.inv | Bin 1910 -> 1959 bytes docs/search/search_index.json | 2 +- docs/sitemap.xml | 12 +- docs/sitemap.xml.gz | Bin 256 -> 256 bytes docs/source/index.html | 45169 +++++++++++++++++++++----------- site/Commands.md | 4 +- site/changelog.md | 14 + site/index.md | 9 +- site/man.md | 18 +- tests/test_utils.py | 8 - torrentfile/__main__.py | 8 + torrentfile/cli.py | 37 +- torrentfile/hasher.py | 10 +- torrentfile/recheck.py | 43 +- tox.ini | 2 +- 25 files changed, 29663 insertions(+), 15837 deletions(-) diff --git a/.gitignore b/.gitignore index 391b44a1..1a081fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -11,9 +11,11 @@ archive/ temp/ .benchmarks/ runner/ +run.py # Byte-compiled / optimized / DLL files *.bin *.pyc +keyboard* __pycache__/ *.py[cod] codacy.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 480aaf99..cc0cbbe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # TorrentFile +## Version 0.7.9 + +- complete rewrite of the recheck procedures +- Recheck now provides more accuracy and more details +- improvements to the new custom progressbar +- changed the cli argument for the progress bar +- the options are now just 0 and 1 +- included new unit tests for all new features +- marked unused functions as deprecated +- added a new hasher object for v2 and hybrid torrents +- minor bug fixes and styling changes + +--------------------- + ## Version 0.7.8 - more updates to logging diff --git a/README.md b/README.md index 7a173a1a..8b369b12 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,12 @@ Commands Distributed under Apache v2 software license. See `LICENSE` for more information. -## 💡 Issues & Requests +## 💡 Issues & Requests & PRs If you encounter any bugs or would like to request a new feature please open a new issue. +> PRs and other contributions are welcome + [https://github.com/alexpdev/torrentfile/issues](https://github.com/alexpdev/torrentfile/issues) ------ @@ -113,11 +115,10 @@ If you encounter any bugs or would like to request a new feature please open a n > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 ``` -The 3 options for controlling the progress bar using `--prog` or `--progress`: +The options for controlling the progress bar using `--prog` or `--progress`: - 0 : show no progress bar at all -- 1 : show 1 progress bar measuring the entire creation process -- 2 : show a progress bar for each file of the torrent content +- 1 : show progress bar (default) ```bash > torrentfile -t http://tracker.com --progress 2 /path/to/content diff --git a/docs/404.html b/docs/404.html index 9e658c0b..fed6dc04 100644 --- a/docs/404.html +++ b/docs/404.html @@ -11,7 +11,7 @@ - + @@ -141,7 +141,7 @@
- +
@@ -185,7 +185,7 @@
- +
@@ -333,7 +333,7 @@

404 - Not found

- + diff --git a/docs/Apache2/index.html b/docs/Apache2/index.html index c02ef645..72e3de1c 100644 --- a/docs/Apache2/index.html +++ b/docs/Apache2/index.html @@ -13,7 +13,7 @@ - + @@ -148,7 +148,7 @@
- +
@@ -192,7 +192,7 @@
- +
@@ -769,7 +769,7 @@

APPENDIX: How to
- + diff --git a/docs/Commands/index.html b/docs/Commands/index.html index 6a327724..dca7dfbf 100644 --- a/docs/Commands/index.html +++ b/docs/Commands/index.html @@ -13,7 +13,7 @@ - + @@ -148,7 +148,7 @@
- +
@@ -192,7 +192,7 @@
- +
@@ -523,8 +523,8 @@

Create

-c <comment>, --comment <comment> Include a comment in file metadata -o <path>, --out <path> Output path for created .torrent file -t <url> [<url> ...], --tracker <url> [<url> ...] One or more Bittorrent tracker announce url(s). ---noprogress Disable showing the progress bar during torrent creation. - (Minimially improves performance of torrent file creation.) +--prog, --progress <level> (0) = no progress bar displayed + (1) = progress bar is displayed (default) --meta-version <int> Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) @@ -661,7 +661,7 @@

Magnet

- + diff --git a/docs/assets/_mkdocstrings.css b/docs/assets/_mkdocstrings.css index b2cceef2..a65078d0 100644 --- a/docs/assets/_mkdocstrings.css +++ b/docs/assets/_mkdocstrings.css @@ -14,3 +14,23 @@ h5.doc-heading { margin-top: 0 !important; margin-bottom: 0 !important; } + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} +.doc .md-typeset__table tr { + display: table-row; +} + +/* Avoid line breaks in rendered fields. */ +.field-body p { + display: inline; +} + +/* Defaults in Spacy table style. */ +.doc-param-default { + float: right; +} diff --git a/docs/changelog/index.html b/docs/changelog/index.html index 3b37bd09..ff01c166 100644 --- a/docs/changelog/index.html +++ b/docs/changelog/index.html @@ -13,7 +13,7 @@ - + @@ -148,7 +148,7 @@
- +
@@ -192,7 +192,7 @@
- +
@@ -291,6 +291,13 @@

+
-

Initiate main function for CLI script.

+

Show torrent metafile details to user via stdout.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + dict +

command line arguements provided by the user.

+ required +
- Source code in torrentfile\cli.py -
def main():
+          Source code in torrentfile\commands.py
+          
 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
def info(args: list):
     """
-    Initiate main function for CLI script.
+    Show torrent metafile details to user via stdout.
+
+    Parameters
+    ----------
+    args : dict
+        command line arguements provided by the user.
     """
-    execute()
+    metafile = args.metafile
+    meta = pyben.load(metafile)
+    info = meta["info"]
+    del meta["info"]
+    meta.update(info)
+    if "private" in meta and meta["private"] == 1:
+        meta["private"] = "True"
+    if "announce-list" in meta:
+        lst = meta["announce-list"]
+        meta["announce-list"] = ", ".join([j for i in lst for j in i])
+    if "url-list" in meta:
+        meta["url-list"] = ", ".join(meta["url-list"])
+    if "httpseeds" in meta:
+        meta["httpseeds"] = ", ".join(meta["httpseeds"])
+    text = []
+    longest = max([len(i) for i in meta.keys()])
+    for key, val in meta.items():
+        if key not in ["pieces", "piece layers", "files", "file tree"]:
+            prefix = longest - len(key) + 1
+            string = key + (" " * prefix) + str(val)
+            text.append(string)
+    most = max([len(i) for i in text])
+    text = ["-" * most, "\n"] + text + ["\n", "-" * most]
+    output = "\n".join(text)
+    print(output)
+    return output
 
+
@@ -1989,384 +2183,208 @@

-

-main_script(args=None) +

+magnet(metafile) -

+ +
-

Initialize Command Line Interface for torrentfile.

+

Create a magnet URI from a Bittorrent meta file.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
argslist

Commandline arguments. default=None

NoneNameTypeDescriptionDefault
+ + + + metafile + + (Namespace||str) + +

Namespace class for CLI arguments.

+ + required + + + + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

created magnet URI.

+
- Source code in torrentfile\cli.py -
def execute(args=None):
+          Source code in torrentfile\commands.py
+          
175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
def magnet(metafile):
     """
-    Initialize Command Line Interface for torrentfile.
+    Create a magnet URI from a Bittorrent meta file.
 
     Parameters
     ----------
-    args : list
-        Commandline arguments. default=None
-    """
-    if not args:
-        if sys.argv[1:]:
-            args = sys.argv[1:]
-        else:
-            args = ["-h"]
+    metafile : (Namespace||str)
+        Namespace class for CLI arguments.
 
-    parser = ArgumentParser(
-        "torrentfile",
-        description=(
-            "Command line tools for creating, editing, checking and "
-            "interacting with Bittorrent metainfo files"
-        ),
-        prefix_chars="-",
-        formatter_class=TorrentFileHelpFormatter,
-        conflict_handler="resolve",
-    )
+    Returns
+    -------
+    str
+        created magnet URI.
+    """
+    if hasattr(metafile, "metafile"):
+        metafile = metafile.metafile
+    if not os.path.exists(metafile):
+        raise FileNotFoundError
 
-    parser.add_argument(
-        "-i",
-        "--interactive",
-        action="store_true",
-        dest="interactive",
-        help="select program options interactively",
-    )
+    meta = pyben.load(metafile)
+    info = meta["info"]
+    binfo = pyben.dumps(info)
+    infohash = sha1(binfo).hexdigest().upper()  # nosec
 
-    parser.add_argument(
-        "-V",
-        "--version",
-        action="version",
-        version=f"torrentfile v{version}",
-        help="show program version and exit",
-    )
+    logger.info("Magnet Info Hash: %s", infohash)
+    scheme = "magnet:"
+    hasharg = "?xt=urn:btih:" + infohash
+    namearg = "&dn=" + quote_plus(info["name"])
 
-    parser.add_argument(
-        "-v",
-        "--verbose",
-        action="store_true",
-        dest="debug",
-        help="output debug information",
-    )
+    if "announce-list" in meta:
+        announce_args = [
+            "&tr=" + quote_plus(url)
+            for urllist in meta["announce-list"]
+            for url in urllist
+        ]
+    else:
+        announce_args = ["&tr=" + quote_plus(meta["announce"])]
 
-    parser.set_defaults(func=parser.print_help)
+    full_uri = "".join([scheme, hasharg, namearg] + announce_args)
+    logger.info("Created Magnet URI %s", full_uri)
+    sys.stdout.write("\n" + full_uri + "\n")
+    return full_uri
+
+
+
+
- subparsers = parser.add_subparsers( - title="Actions", - dest="command", - metavar="create, edit, magnet, recheck", - ) +
- create_parser = subparsers.add_parser( - "create", - help="""Generate a new torrent meta file.""", - prefix_chars="-", - aliases=["c", "new"], - formatter_class=TorrentFileHelpFormatter, - ) - create_parser.add_argument( - "-a", - "-t", - "--announce", - "--tracker", - action="store", - dest="announce", - metavar="<url>", - nargs="+", - default=[], - help="One or more space-seperated torrent tracker url(s).", - ) - create_parser.add_argument( - "-p", - "--private", - action="store_true", - dest="private", - help="Creates private torrent with multi-tracker and DHT turned off.", - ) - create_parser.add_argument( - "-s", - "--source", - action="store", - dest="source", - metavar="<source>", - help="Add a source string. Useful for cross-seeding.", - ) +
- create_parser.add_argument( - "-m", - "--magnet", - action="store_true", - dest="magnet", - ) - create_parser.add_argument( - "-c", - "--comment", - action="store", - dest="comment", - metavar="<comment>", - help="Include a comment in file metadata", - ) - create_parser.add_argument( - "-o", - "--out", - action="store", - dest="outfile", - metavar="<path>", - help="Output save path for created .torrent file", - ) +

+ __main__ - create_parser.add_argument( - "--cwd", - "--current", - action="store_true", - dest="cwd", - help="Save output .torrent file to current directory", - ) - create_parser.add_argument( - "--prog", - "--progress", - default="1", - action="store", - dest="progress", - help=""" - Set the progress bar level. - Options = 0, 1 - (0) = Do not display progress bar. - (1) = Display progress bar. - """, - ) - create_parser.add_argument( - "--meta-version", - default="1", - choices=["1", "2", "3"], - action="store", - dest="meta_version", - metavar="<int>", - help=""" - Bittorrent metafile version. - Options = 1, 2, 3 - (1) = Bittorrent v1 (Default) - (2) = Bittorrent v2 - (3) = Bittorrent v1 & v2 hybrid - """, - ) +

- create_parser.add_argument( - "--piece-length", - action="store", - dest="piece_length", - metavar="<int>", - help=""" - (Default: <blank>) Number of bytes for per chunk of data transmitted - by Bittorrent client. Acceptable values include integers 14-26 which - will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. - Examples:: [--piece-length 14] [--piece-length 20] - """, - ) +
- create_parser.add_argument( - "-w", - "--web-seed", - action="store", - dest="url_list", - metavar="<url>", - nargs="+", - help="list of web addresses where torrent data exists (GetRight).", - ) +

Enable calling the package directly with python from the command line.

- create_parser.add_argument( - "--http-seed", - action="store", - dest="httpseeds", - metavar="<url>", - nargs="+", - help="list of URLs, addresses where content can be found (Hoffman).", - ) - create_parser.add_argument( - "content", - action="store", - metavar="<content>", - nargs="?", - help="Path to content file or directory", - ) - create_parser.set_defaults(func=create) +
- edit_parser = subparsers.add_parser( - "edit", - help=""" - Edit existing torrent meta file. - """, - aliases=["e"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) - edit_parser.add_argument( - "metafile", - action="store", - help="path to *.torrent file", - metavar="<*.torrent>", - ) - edit_parser.add_argument( - "--tracker", - action="store", - dest="announce", - metavar="<url>", - nargs="+", - help=""" - Replace current list of tracker/announce urls with one or more space - seperated Bittorrent tracker announce url(s). - """, - ) - edit_parser.add_argument( - "--web-seed", - action="store", - dest="url_list", - metavar="<url>", - nargs="+", - help="Replace current list of web-seed urls with one or more url(s)", - ) - edit_parser.add_argument( - "--http-seed", - action="store", - dest="httpseeds", - metavar="<url>", - nargs="+", - help="replace all currently listed addresses with new list (Hoffman).", - ) - edit_parser.add_argument( - "--private", - action="store_true", - help="Make torrent private.", - dest="private", - ) - edit_parser.add_argument( - "--comment", - help="Replaces any existing comment with <comment>", - metavar="<comment>", - dest="comment", - action="store", - ) - edit_parser.add_argument( - "--source", - action="store", - dest="source", - metavar="<source>", - help="Replaces current source with <source>", - ) +
- edit_parser.set_defaults(func=edit) - magnet_parser = subparsers.add_parser( - "magnet", - help=""" - Generate magnet url from an existing Bittorrent meta file. - """, - aliases=["m"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) - magnet_parser.add_argument( - "metafile", - action="store", - help="Path to Bittorrent meta file.", - metavar="<*.torrent>", - ) +

+main() - magnet_parser.set_defaults(func=magnet) - check_parser = subparsers.add_parser( - "recheck", - help=""" - Calculate amount of torrent meta file's content is found on disk. - """, - aliases=["r", "check"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) - - check_parser.add_argument( - "metafile", - action="store", - metavar="<*.torrent>", - help="path to .torrent file.", - ) - - check_parser.add_argument( - "content", - action="store", - metavar="<content>", - help="path to content file or directory", - ) - - check_parser.set_defaults(func=recheck) - - info_parser = subparsers.add_parser( - "info", - help=""" - Show detailed information about a torrent file. - """, - aliases=["i"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) - - info_parser.add_argument( - "metafile", - action="store", - metavar="<*.torrent>", - help="path to pre-existing torrent file.", - ) - - info_parser.set_defaults(func=info) +

- args = parser.parse_args(args) - if args.debug: - activate_logger() +
- if args.interactive: - return select_action() +

Start the entry point script.

- return args.func(args) +
+ Source code in torrentfile\__main__.py +
25
+26
+27
+28
+29
def main():
+    """
+    Start the entry point script.
+    """
+    execute()
 
+
@@ -2376,7 +2394,6 @@

-

@@ -2389,8 +2406,8 @@

-

- commands +

+ cli @@ -2398,15 +2415,13 @@

-

The commands module contains the Action Commands executed by the CLI script.

-

Each function pertains to a command line action/subcommand and drives specific -features of the application.

-
Functions
-

create_command -info_command -edit_command -recheck_command -magnet_command

+

Command Line Interface for TorrentFile project.

+

This module provides the primary command line argument parser for +the torrentfile package. The main_script function is automatically +invoked when called from command line, and parses accompanying arguments.

+

Functions: + main_script: process command line arguments and run program. + activate_logger: turns on debug mode and logging facility.

@@ -2419,179 +2434,232 @@
Functions
+
-
- +

+ TorrentFileHelpFormatter -

-create(args)

+
+

+ Bases: HelpFormatter

-

Execute the create CLI sub-command to create a new torrent metafile.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
argslist

positional and optional CLI arguments.

required
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
torrentfile.MetaFile

object containing the path to created metafile and its contents.

-
- Source code in torrentfile\commands.py -
def create(args: list):
-    """
-    Execute the create CLI sub-command to create a new torrent metafile.
+      

Formatting class for help tips provided by the CLI.

+

Subclasses Argparse.HelpFormatter.

- Parameters - ---------- - args : argparse.Namespace - positional and optional CLI arguments. - Returns - ------- - torrentfile.MetaFile - object containing the path to created metafile and its contents. +
+ Source code in torrentfile\cli.py +
 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
class TorrentFileHelpFormatter(HelpFormatter):
+    """
+    Formatting class for help tips provided by the CLI.
+
+    Subclasses Argparse.HelpFormatter.
     """
-    kwargs = vars(args)
-    logger.debug("Creating torrent from %s", args.content)
-    if args.meta_version == "1":
-        torrent = TorrentFile(**kwargs)
-    else:
-        torrent = TorrentAssembler(**kwargs)
-    outfile, meta = torrent.write()
 
-    if args.magnet:
-        magnet(outfile)
+    def __init__(self, prog, width=40, max_help_positions=30):
+        """
+        Construct HelpFormat class for usage output.
 
-    args.torrent = torrent
-    args.kwargs = kwargs
-    args.outfile = outfile
-    args.meta = meta
+        Parameters
+        ----------
+        prog : str
+            Name of the program.
+        width : int
+            Max width of help message output.
+        max_help_positions : int
+            max length until line wrap.
+        """
+        super().__init__(
+            prog, width=width, max_help_position=max_help_positions
+        )
 
-    print("\nTorrent Save Path: ", os.path.abspath(str(outfile)))
-    logger.debug("Output path: %s", str(outfile))
-    return args
-
- - + def _split_lines(self, text, _): + """ + Split multiline help messages and remove indentation. - + Parameters + ---------- + text : str + text that needs to be split + _ : int + max width for line. + """ + lines = text.split("\n") + return [line.strip() for line in lines if line] + def _format_text(self, text): + """ + Format text for cli usage messages. + Parameters + ---------- + text : str + Pre-formatted text. -
+ Returns + ------- + str + Formatted text from input. + """ + text = text % dict(prog=self._prog) if "%(prog)" in text else text + text = self._whitespace_matcher.sub(" ", text).strip() + return text + "\n\n" + def _join_parts(self, part_strings): + """ + Combine different sections of the help message. + Parameters + ---------- + part_strings : list + List of argument help messages and headers. -

-edit(args) + Returns + ------- + str + Fully formatted help message for CLI. + """ + parts = self.format_headers(part_strings) + return super()._join_parts(parts) + @staticmethod + def format_headers(parts): + """ + Format help message section headers. -

+ Parameters + ---------- + parts : list + List of individual lines for help message. -
+ Returns + ------- + list + Input list with formatted section headers. + """ + if parts and parts[0].startswith("usage:"): + parts[0] = "Usage\n=====\n " + parts[0][6:] + headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")] + for i in headings[::-1]: + parts[i] = parts[i][:-2].title() + underline = "".join(["\n", "-" * len(parts[i]), "\n"]) + parts.insert(i + 1, underline) + return parts +
+
+
+ + + +
-

Execute the edit CLI sub-command with provided arguments.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
argslist

positional and optional CLI arguments.

required
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

path to edited torrent file.

-
- Source code in torrentfile\commands.py -
def edit(args: list):
-    """
-    Execute the edit CLI sub-command with provided arguments.
 
-    Parameters
-    ----------
-    args : Namespace
-        positional and optional CLI arguments.
 
-    Returns
-    -------
-    str
-        path to edited torrent file.
-    """
-    metafile = args.metafile
-    logger.info("Editing %s Meta File", str(args.metafile))
-    editargs = {
-        "url-list": args.url_list,
-        "httpseeds": args.httpseeds,
-        "announce": args.announce,
-        "source": args.source,
-        "private": args.private,
-        "comment": args.comment,
-    }
-    return edit_torrent(metafile, editargs)
-
-
-
-
@@ -2599,73 +2667,96 @@

-

-info(args) +

+__init__(prog, width=40, max_help_positions=30) -
+

+
-

Show torrent metafile details to user via stdout.

+

Construct HelpFormat class for usage output.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
argslist

command line arguements provided by the user.

requiredNameTypeDescriptionDefault
+ + + + prog + + str + +

Name of the program.

+ + required + + + + width + + int + +

Max width of help message output.

+ + 40 + + + + max_help_positions + + int + +

max length until line wrap.

+ + 30 + + + + +
- Source code in torrentfile\commands.py -
def info(args: list):
+          Source code in torrentfile\cli.py
+          
77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
def __init__(self, prog, width=40, max_help_positions=30):
     """
-    Show torrent metafile details to user via stdout.
+    Construct HelpFormat class for usage output.
 
     Parameters
     ----------
-    args : dict
-        command line arguements provided by the user.
+    prog : str
+        Name of the program.
+    width : int
+        Max width of help message output.
+    max_help_positions : int
+        max length until line wrap.
     """
-    metafile = args.metafile
-    meta = pyben.load(metafile)
-    info = meta["info"]
-    del meta["info"]
-    meta.update(info)
-    if "private" in meta and meta["private"] == 1:
-        meta["private"] = "True"
-    if "announce-list" in meta:
-        lst = meta["announce-list"]
-        meta["announce-list"] = ", ".join([j for i in lst for j in i])
-    if "url-list" in meta:
-        meta["url-list"] = ", ".join(meta["url-list"])
-    if "httpseeds" in meta:
-        meta["httpseeds"] = ", ".join(meta["httpseeds"])
-    text = []
-    longest = max([len(i) for i in meta.keys()])
-    for key, val in meta.items():
-        if key not in ["pieces", "piece layers", "files", "file tree"]:
-            prefix = longest - len(key) + 1
-            string = key + (" " * prefix) + str(val)
-            text.append(string)
-    most = max([len(i) for i in text])
-    text = ["-" * most, "\n"] + text + ["\n", "-" * most]
-    output = "\n".join(text)
-    print(output)
-    return output
+    super().__init__(
+        prog, width=width, max_help_position=max_help_positions
+    )
 
+
@@ -2677,95 +2768,96 @@

-

-magnet(metafile) +

+_format_text(text) -
+ +
-

Create a magnet URI from a Bittorrent meta file.

+

Format text for cli usage messages.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
text + str +

Pre-formatted text.

+ required +
+ +

Returns:

+ + - - - - + + - -
metafile(Namespace||str)

Namespace class for CLI arguments.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

created magnet URI.

+ + + + + str + +

Formatted text from input.

+ + + +
- Source code in torrentfile\commands.py -
def magnet(metafile):
-    """
-    Create a magnet URI from a Bittorrent meta file.
+          Source code in torrentfile\cli.py
+          
108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
def _format_text(self, text):
+    """
+    Format text for cli usage messages.
 
     Parameters
     ----------
-    metafile : (Namespace||str)
-        Namespace class for CLI arguments.
+    text : str
+        Pre-formatted text.
 
     Returns
     -------
     str
-        created magnet URI.
+        Formatted text from input.
     """
-    if hasattr(metafile, "metafile"):
-        metafile = metafile.metafile
-    if not os.path.exists(metafile):
-        raise FileNotFoundError
-
-    meta = pyben.load(metafile)
-    info = meta["info"]
-    binfo = pyben.dumps(info)
-    infohash = sha1(binfo).hexdigest().upper()  # nosec
-
-    logger.info("Magnet Info Hash: %s", infohash)
-    scheme = "magnet:"
-    hasharg = "?xt=urn:btih:" + infohash
-    namearg = "&dn=" + quote_plus(info["name"])
-
-    if "announce-list" in meta:
-        announce_args = [
-            "&tr=" + quote_plus(url)
-            for urllist in meta["announce-list"]
-            for url in urllist
-        ]
-    else:
-        announce_args = ["&tr=" + quote_plus(meta["announce"])]
-
-    full_uri = "".join([scheme, hasharg, namearg] + announce_args)
-    logger.info("Created Magnet URI %s", full_uri)
-    sys.stdout.write("\n" + full_uri + "\n")
-    return full_uri
+    text = text % dict(prog=self._prog) if "%(prog)" in text else text
+    text = self._whitespace_matcher.sub(" ", text).strip()
+    return text + "\n\n"
 
+
@@ -2777,78 +2869,94 @@

-

-recheck(args) +

+_join_parts(part_strings) -
+ +
-

Execute recheck CLI sub-command.

+

Combine different sections of the help message.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
part_strings + list +

List of argument help messages and headers.

+ required +
+ +

Returns:

+ + - - - - + + - -
argsNamespace

positional and optional arguments.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

The percentage of content currently saved to disk.

+ + + + + str + +

Fully formatted help message for CLI.

+ + + +
- Source code in torrentfile\commands.py -
def recheck(args):
-    """
-    Execute recheck CLI sub-command.
+          Source code in torrentfile\cli.py
+          
126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
def _join_parts(self, part_strings):
+    """
+    Combine different sections of the help message.
 
     Parameters
     ----------
-    args : Namespace
-        positional and optional arguments.
+    part_strings : list
+        List of argument help messages and headers.
 
     Returns
     -------
     str
-        The percentage of content currently saved to disk.
+        Fully formatted help message for CLI.
     """
-    metafile = args.metafile
-    content = args.content
-    logger.debug("Validating %s against %s contents", metafile, content)
-    checker = Checker(metafile, content)
-
-    logger.debug("Completed initialization of the Checker class")
-    result = checker.results()
-
-    sys.stdout.write(str(result) + "% Match\n")
-    sys.stdout.flush()
-    return result
+    parts = self.format_headers(part_strings)
+    return super()._join_parts(parts)
 
+
@@ -2856,53 +2964,88 @@

+
-
- -

- -
- - - -
- - - -

- edit +

+_split_lines(text, _) +
-
-

Edit torrent module.

-

Provides a facility by which certain properties of a torrent meta file can be -edited by the user. The various command line arguments indicate which fields -should be edited, and what the new value should be. Depending on what fields -are chosen to edit, this command can trigger a new info hash which means the -torrent will no longer be able to participate in the same swarm as the original -unedited torrent.

-
Keywords
-

private -comment -source -trackers -web-seeds

- - - -
- - +

Split multiline help messages and remove indentation.

+

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
text + str +

text that needs to be split

+ required +
_ + int +

max width for line.

+ required +
+
+ Source code in torrentfile\cli.py +
 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
def _split_lines(self, text, _):
+    """
+    Split multiline help messages and remove indentation.
 
+    Parameters
+    ----------
+    text : str
+        text that needs to be split
+    _ : int
+        max width for line.
+    """
+    lines = text.split("\n")
+    return [line.strip() for line in lines if line]
+
+
+
+
+
@@ -2910,197 +3053,203 @@
Keywords
-

-edit_torrent(metafile, args) +

+format_headers(parts) + + staticmethod + + +
-
-

Edit the properties and values in a torrent meta file.

+

Format help message section headers.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + + + + + + + + + + +
metafilestr

path to the torrent meta file.

requiredNameTypeDescriptionDefault
parts + list +

List of individual lines for help message.

+ required +
+ +

Returns:

+ + - - - - + + - -
argsdict

key value pairs of the properties to be edited.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
dict

The edited and nested Meta and info dictionaries.

+ + + + + list + +

Input list with formatted section headers.

+ + + +
- Source code in torrentfile\edit.py -
def edit_torrent(metafile: str, args: dict) -> dict:
+          Source code in torrentfile\cli.py
+          
143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
@staticmethod
+def format_headers(parts):
     """
-    Edit the properties and values in a torrent meta file.
+    Format help message section headers.
 
     Parameters
     ----------
-    metafile : str
-        path to the torrent meta file.
-    args : dict
-        key value pairs of the properties to be edited.
+    parts : list
+        List of individual lines for help message.
 
     Returns
     -------
-    dict
-        The edited and nested Meta and info dictionaries.
+    list
+        Input list with formatted section headers.
     """
-    logger.debug("editing torrent file %s", metafile)
-    meta = pyben.load(metafile)
-    info = meta["info"]
-    filter_empty(args, meta, info)
+    if parts and parts[0].startswith("usage:"):
+        parts[0] = "Usage\n=====\n  " + parts[0][6:]
+    headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
+    for i in headings[::-1]:
+        parts[i] = parts[i][:-2].title()
+        underline = "".join(["\n", "-" * len(parts[i]), "\n"])
+        parts.insert(i + 1, underline)
+    return parts
+
+
+
+
- if "comment" in args: - info["comment"] = args["comment"] +
- if "source" in args: - info["source"] = args["source"] - if "private" in args: - info["private"] = 1 - if "announce" in args: - val = args.get("announce", None) - if isinstance(val, str): - vallist = val.split() - meta["announce"] = vallist[0] - meta["announce-list"] = [vallist] - elif isinstance(val, list): - meta["announce"] = val[0] - meta["announce-list"] = [val] - if "url-list" in args: - val = args.get("url-list") - if isinstance(val, str): - meta["url-list"] = val.split() - elif isinstance(val, list): - meta["url-list"] = val - if "httpseeds" in args: - val = args.get("httpseeds") - if isinstance(val, str): - meta["httpseeds"] = val.split() - elif isinstance(val, list): - meta["httpseeds"] = val +
- meta["info"] = info - os.remove(metafile) - pyben.dump(meta, metafile) - return meta -
-
+
-

-filter_empty(args, meta, info) +

+activate_logger()

+
-

Remove dictionary keys with empty values.

+

Activate the builtin logging mechanism when passed debug flag from CLI.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
argsdict

Editable metafile properties from user.

required
metadict

Metafile data dictionary.

required
infodict

Metafile info dictionary.

required
- Source code in torrentfile\edit.py -
def filter_empty(args: dict, meta: dict, info: dict):
+          Source code in torrentfile\cli.py
+          
40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
def activate_logger():
     """
-    Remove dictionary keys with empty values.
-
-    Parameters
-    ----------
-    args : dict
-        Editable metafile properties from user.
-    meta : dict
-        Metafile data dictionary.
-    info : dict
-        Metafile info dictionary.
+    Activate the builtin logging mechanism when passed debug flag from CLI.
     """
-    for key, val in list(args.items()):
-        if val is None:
-            del args[key]
-            continue
-
-        if val == "":
-            if key in meta:
-                del meta[key]
-            elif key in info:
-                del info[key]
-            del args[key]
-            logger.debug("removeing empty fields %s", val)
+    logging.basicConfig(level=logging.INFO)
+    logger = logging.getLogger()
+    # file_handler = logging.FileHandler(
+    #     "torrentfile.log", mode="a+", encoding="utf-8"
+    # )
+    console_handler = logging.StreamHandler(stream=sys.stderr)
+    # file_formatter = logging.Formatter(
+    #     "%(asctime)s %(levelno)s %(message)s",
+    #     datefmt="%m-%d %H:%M:%S",
+    #     style="%",
+    # )
+    stream_formatter = logging.Formatter(
+        "%(asctime)s %(levelno)s %(message)s",
+        datefmt="%m-%d %H:%M:%S",
+        style="%",
+    )
+    # file_handler.setFormatter(file_formatter)
+    console_handler.setFormatter(stream_formatter)
+    # file_handler.setLevel(logging.INFO)
+    console_handler.setLevel(logging.DEBUG)
+    logger.setLevel(logging.DEBUG)
+    logger.addHandler(console_handler)
+    # logger.addHandler(file_handler)
+    logger.debug("Debug: ON")
 
+
@@ -3108,294 +3257,807 @@

+
-
+

+execute(args=None) -

-
+ +
-
+

Initialize Command Line Interface for torrentfile.

+

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + list +

Commandline arguments. default=None

+ None +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list +

Depends on what the command line args were.

+
+ Source code in torrentfile\cli.py +
168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
def execute(args=None) -> list:
+    """
+    Initialize Command Line Interface for torrentfile.
 
-

- hasher + Parameters + ---------- + args : list + Commandline arguments. default=None + Returns + ------- + list + Depends on what the command line args were. + """ + if not args: + if sys.argv[1:]: + args = sys.argv[1:] + else: + args = ["-h"] + parser = ArgumentParser( + "torrentfile", + description=( + "Command line tools for creating, editing, checking and " + "interacting with Bittorrent metainfo files" + ), + prefix_chars="-", + formatter_class=TorrentFileHelpFormatter, + conflict_handler="resolve", + ) -

+ parser.add_argument( + "-i", + "--interactive", + action="store_true", + dest="interactive", + help="select program options interactively", + ) -
+ parser.add_argument( + "-V", + "--version", + action="version", + version=f"torrentfile v{version}", + help="show program version and exit", + ) -

Piece/File Hashers for Bittorrent meta file contents.

+ parser.add_argument( + "-v", + "--verbose", + action="store_true", + dest="debug", + help="output debug information", + ) + parser.set_defaults(func=parser.print_help) + subparsers = parser.add_subparsers( + title="Actions", + dest="command", + metavar="create, edit, magnet, recheck", + ) -
+ create_parser = subparsers.add_parser( + "create", + help="""Generate a new torrent meta file.""", + prefix_chars="-", + aliases=["c", "new"], + formatter_class=TorrentFileHelpFormatter, + ) + create_parser.add_argument( + "-a", + "-t", + "--announce", + "--tracker", + action="store", + dest="announce", + metavar="<url>", + nargs="+", + default=[], + help="One or more space-seperated torrent tracker url(s).", + ) + create_parser.add_argument( + "-p", + "--private", + action="store_true", + dest="private", + help="Creates private torrent with multi-tracker and DHT turned off.", + ) + create_parser.add_argument( + "-s", + "--source", + action="store", + dest="source", + metavar="<source>", + help="Add a source string. Useful for cross-seeding.", + ) + create_parser.add_argument( + "-m", + "--magnet", + action="store_true", + dest="magnet", + ) + create_parser.add_argument( + "-c", + "--comment", + action="store", + dest="comment", + metavar="<comment>", + help="Include a comment in file metadata", + ) + create_parser.add_argument( + "-o", + "--out", + action="store", + dest="outfile", + metavar="<path>", + help="Output save path for created .torrent file", + ) + create_parser.add_argument( + "--cwd", + "--current", + action="store_true", + dest="cwd", + help="Save output .torrent file to current directory", + ) + create_parser.add_argument( + "--prog", + "--progress", + default="1", + action="store", + dest="progress", + help=""" + Set the progress bar level. + Options = 0, 1 + (0) = Do not display progress bar. + (1) = Display progress bar. + """, + ) + create_parser.add_argument( + "--meta-version", + default="1", + choices=["1", "2", "3"], + action="store", + dest="meta_version", + metavar="<int>", + help=""" + Bittorrent metafile version. + Options = 1, 2, 3 + (1) = Bittorrent v1 (Default) + (2) = Bittorrent v2 + (3) = Bittorrent v1 & v2 hybrid + """, + ) -
+ create_parser.add_argument( + "--piece-length", + action="store", + dest="piece_length", + metavar="<int>", + help=""" + (Default: <blank>) Number of bytes for per chunk of data transmitted + by Bittorrent client. Acceptable values include integers 14-26 which + will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. + Examples:: [--piece-length 14] [--piece-length 20] + """, + ) + create_parser.add_argument( + "-w", + "--web-seed", + action="store", + dest="url_list", + metavar="<url>", + nargs="+", + help="list of web addresses where torrent data exists (GetRight).", + ) + create_parser.add_argument( + "--http-seed", + action="store", + dest="httpseeds", + metavar="<url>", + nargs="+", + help="list of URLs, addresses where content can be found (Hoffman).", + ) -

- -FileHasher (CbMixin, ProgMixin) - + create_parser.add_argument( + "content", + action="store", + metavar="<content>", + nargs="?", + help="Path to content file or directory", + ) + create_parser.set_defaults(func=create) + edit_parser = subparsers.add_parser( + "edit", + help=""" + Edit existing torrent meta file. + """, + aliases=["e"], + prefix_chars="-", + formatter_class=TorrentFileHelpFormatter, + ) -

+ edit_parser.add_argument( + "metafile", + action="store", + help="path to *.torrent file", + metavar="<*.torrent>", + ) -
+ edit_parser.add_argument( + "--tracker", + action="store", + dest="announce", + metavar="<url>", + nargs="+", + help=""" + Replace current list of tracker/announce urls with one or more space + seperated Bittorrent tracker announce url(s). + """, + ) -

Calculate root and piece hashes for creating hybrid torrent file.

-

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. -With a branching factor of 2, merge layer hashes until blocks equal -piece_length bytes for the piece layer, and then the root hash.

- -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
pathstr

path to target file.

required
piece_lengthint

piece length for data chunks.

required
progressbool

default = None

True
-
- Source code in torrentfile\hasher.py -
class FileHasher(CbMixin, ProgMixin):
-    """
-    Calculate root and piece hashes for creating hybrid torrent file.
-
-    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
-    With a branching factor of 2, merge layer hashes until blocks equal
-    piece_length bytes for the piece layer, and then the root hash.
+    edit_parser.add_argument(
+        "--web-seed",
+        action="store",
+        dest="url_list",
+        metavar="<url>",
+        nargs="+",
+        help="Replace current list of web-seed urls with one or more url(s)",
+    )
 
-    Parameters
-    ----------
-    path : str
-        path to target file.
-    piece_length : int
-        piece length for data chunks.
-    progress : int
-        default = None
-    """
+    edit_parser.add_argument(
+        "--http-seed",
+        action="store",
+        dest="httpseeds",
+        metavar="<url>",
+        nargs="+",
+        help="replace all currently listed addresses with new list (Hoffman).",
+    )
 
-    def __init__(
-        self,
-        path: str,
-        piece_length: int,
-        progress: bool = True,
-        hybrid: bool = False,
-    ):
-        """
-        Construct Hasher class instances for each file in torrent.
-        """
-        self.path = path
-        self.piece_length = piece_length
-        self.pieces = []
-        self.layer_hashes = []
-        self.piece_layer = None
-        self.root = None
-        self.padding_piece = None
-        self.padding_file = None
-        self.amount = piece_length // BLOCK_SIZE
-        self.end = False
-        self.current = open(path, "rb")
-        self.hybrid = hybrid
-        if progress:
-            self.progressbar = True
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
+    edit_parser.add_argument(
+        "--private",
+        action="store_true",
+        help="Make torrent private.",
+        dest="private",
+    )
 
-    def __iter__(self):
-        """Return `self`: needed to implement iterator implementation."""
-        return self
+    edit_parser.add_argument(
+        "--comment",
+        help="Replaces any existing comment with <comment>",
+        metavar="<comment>",
+        dest="comment",
+        action="store",
+    )
 
-    def _pad_remaining(self, block_count: int):
-        """
-        Generate Hash sized, 0 filled bytes for padding.
+    edit_parser.add_argument(
+        "--source",
+        action="store",
+        dest="source",
+        metavar="<source>",
+        help="Replaces current source with <source>",
+    )
 
-        Parameters
-        ----------
-        block_count : int
-            current total number of blocks collected.
+    edit_parser.set_defaults(func=edit)
 
-        Returns
-        -------
-        padding : bytes
-            Padding to fill remaining portion of tree.
-        """
-        # when the there is only one block for file
-        remaining = self.amount - block_count
-        if not self.layer_hashes:
-            power2 = next_power_2(block_count)
-            remaining = power2 - block_count
-        self.prog_update(HASH_SIZE * remaining)
-        return [bytes(HASH_SIZE) for _ in range(remaining)]
+    magnet_parser = subparsers.add_parser(
+        "magnet",
+        help="""
+        Generate magnet url from an existing Bittorrent meta file.
+        """,
+        aliases=["m"],
+        prefix_chars="-",
+        formatter_class=TorrentFileHelpFormatter,
+    )
 
-    def __next__(self):
-        """
-        Calculate layer hashes for contents of file.
+    magnet_parser.add_argument(
+        "metafile",
+        action="store",
+        help="Path to Bittorrent meta file.",
+        metavar="<*.torrent>",
+    )
 
-        Parameters
-        ----------
-        data : BytesIO
-            File opened in read mode.
-        """
-        if self.end:
-            self.end = False
-            raise StopIteration
-        plength = self.piece_length
-        blocks = []
-        piece = sha1()  # nosec
-        total = 0
-        block = bytearray(BLOCK_SIZE)
-        for _ in range(self.amount):
-            size = self.current.readinto(block)
-            self.prog_update(size)
-            if not size:
-                self.end = True
-                break
-            total += size
-            plength -= size
-            blocks.append(sha256(block[:size]).digest())
-            if self.hybrid:
-                piece.update(block[:size])
-        if len(blocks) != self.amount:
-            padding = self._pad_remaining(len(blocks))
-            blocks.extend(padding)
-        layer_hash = merkle_root(blocks)
-        self.layer_hashes.append(layer_hash)
-        if self._cb:
-            self._cb(layer_hash)
-        if self.end:
-            self._calculate_root()
-            self.prog_close()
-        if self.hybrid:
-            if plength > 0:
-                self.padding_file = {
-                    "attr": "p",
-                    "length": plength,
-                    "path": [".pad", str(plength)],
-                }
-                piece.update(bytes(plength))
-            piece = piece.digest()
-            self.pieces.append(piece)
-            return layer_hash, piece
-        return layer_hash
+    magnet_parser.set_defaults(func=magnet)
 
-    def _calculate_root(self):
-        """
-        Calculate the root hash for opened file.
-        """
-        self.piece_layer = b"".join(self.layer_hashes)
+    check_parser = subparsers.add_parser(
+        "recheck",
+        help="""
+        Calculate amount of torrent meta file's content is found on disk.
+        """,
+        aliases=["r", "check"],
+        prefix_chars="-",
+        formatter_class=TorrentFileHelpFormatter,
+    )
 
-        if len(self.layer_hashes) > 1:
-            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
+    check_parser.add_argument(
+        "metafile",
+        action="store",
+        metavar="<*.torrent>",
+        help="path to .torrent file.",
+    )
 
-            pow2 = next_power_2(len(self.layer_hashes))
-            remainder = pow2 - len(self.layer_hashes)
+    check_parser.add_argument(
+        "content",
+        action="store",
+        metavar="<content>",
+        help="path to content file or directory",
+    )
 
-            self.layer_hashes += [pad_piece for _ in range(remainder)]
-        self.root = merkle_root(self.layer_hashes)
-        self.current.close()
-
-
+ check_parser.set_defaults(func=recheck) + info_parser = subparsers.add_parser( + "info", + help=""" + Show detailed information about a torrent file. + """, + aliases=["i"], + prefix_chars="-", + formatter_class=TorrentFileHelpFormatter, + ) + info_parser.add_argument( + "metafile", + action="store", + metavar="<*.torrent>", + help="path to pre-existing torrent file.", + ) -
+ info_parser.set_defaults(func=info) + args = parser.parse_args(args) + if args.debug: + activate_logger() + if args.interactive: + return select_action() + if hasattr(args, "func"): + return args.func(args) + return args +
+
+
+
+
+
-
+

+main() -

-__init__(self, path, piece_length, progress=True, hybrid=False) - - special - +
-
-

Construct Hasher class instances for each file in torrent.

+

Initiate main function for CLI script.

- Source code in torrentfile\hasher.py -
def __init__(
-    self,
-    path: str,
-    piece_length: int,
-    progress: bool = True,
-    hybrid: bool = False,
-):
+          Source code in torrentfile\cli.py
+          
526
+527
+528
+529
+530
def main():
     """
-    Construct Hasher class instances for each file in torrent.
+    Initiate main function for CLI script.
     """
-    self.path = path
-    self.piece_length = piece_length
-    self.pieces = []
-    self.layer_hashes = []
-    self.piece_layer = None
-    self.root = None
-    self.padding_piece = None
-    self.padding_file = None
-    self.amount = piece_length // BLOCK_SIZE
-    self.end = False
-    self.current = open(path, "rb")
-    self.hybrid = hybrid
-    if progress:
-        self.progressbar = True
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
+    execute()
 
+
@@ -3403,350 +4065,565 @@
-
+
-
-__iter__(self) +
+ +
+ + + +
- - special - - + +

+ commands + + + +

-

Return self: needed to implement iterator implementation.

+

The commands module contains the Action Commands executed by the CLI script.

+

Each function pertains to a command line action/subcommand and drives specific +features of the application.

+
Functions
+

create_command +info_command +edit_command +recheck_command +magnet_command

-
- Source code in torrentfile\hasher.py -
def __iter__(self):
-    """Return `self`: needed to implement iterator implementation."""
-    return self
-
-
-
-
+ +
+ -
-
-__next__(self) - - special - -
+ +
+ + + +

+create(args) + + +

+
-

Calculate layer hashes for contents of file.

+

Execute the create CLI sub-command to create a new torrent metafile.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + argparse.Namespace +

positional and optional CLI arguments.

+ required +
+ +

Returns:

+ + - - - - + + - -
dataBytesIO

File opened in read mode.

requiredTypeDescription
+ + + + +
torrentfile.MetaFile + +

object containing the path to created metafile and its contents.

+ + + +
- Source code in torrentfile\hasher.py -
def __next__(self):
+          Source code in torrentfile\commands.py
+          
49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
def create(args: list):
     """
-    Calculate layer hashes for contents of file.
+    Execute the create CLI sub-command to create a new torrent metafile.
 
     Parameters
     ----------
-    data : BytesIO
-        File opened in read mode.
-    """
-    if self.end:
-        self.end = False
-        raise StopIteration
-    plength = self.piece_length
-    blocks = []
-    piece = sha1()  # nosec
-    total = 0
-    block = bytearray(BLOCK_SIZE)
-    for _ in range(self.amount):
-        size = self.current.readinto(block)
-        self.prog_update(size)
-        if not size:
-            self.end = True
-            break
-        total += size
-        plength -= size
-        blocks.append(sha256(block[:size]).digest())
-        if self.hybrid:
-            piece.update(block[:size])
-    if len(blocks) != self.amount:
-        padding = self._pad_remaining(len(blocks))
-        blocks.extend(padding)
-    layer_hash = merkle_root(blocks)
-    self.layer_hashes.append(layer_hash)
-    if self._cb:
-        self._cb(layer_hash)
-    if self.end:
-        self._calculate_root()
-        self.prog_close()
-    if self.hybrid:
-        if plength > 0:
-            self.padding_file = {
-                "attr": "p",
-                "length": plength,
-                "path": [".pad", str(plength)],
-            }
-            piece.update(bytes(plength))
-        piece = piece.digest()
-        self.pieces.append(piece)
-        return layer_hash, piece
-    return layer_hash
-
- - - - - - + args : argparse.Namespace + positional and optional CLI arguments. + Returns + ------- + torrentfile.MetaFile + object containing the path to created metafile and its contents. + """ + kwargs = vars(args) + logger.debug("Creating torrent from %s", args.content) + if args.meta_version == "1": + torrent = TorrentFile(**kwargs) + else: + torrent = TorrentAssembler(**kwargs) + outfile, meta = torrent.write() + if args.magnet: + magnet(outfile) - + args.torrent = torrent + args.kwargs = kwargs + args.outfile = outfile + args.meta = meta + print("\nTorrent Save Path: ", os.path.abspath(str(outfile))) + logger.debug("Output path: %s", str(outfile)) + return args + +
+
-
- +
-

- -Hasher (CbMixin, ProgMixin) - +

+edit(args)

+
-

Piece hasher for Bittorrent V1 files.

-

Takes a sorted list of all file paths, calculates sha1 hash -for fixed size pieces of file data from each file -seemlessly until the last piece which may be smaller than others.

+

Execute the edit CLI sub-command with provided arguments.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
pathslist

List of files.

required
+ - - - - + + + + + + + + + + + + + +
piece_lengthint

Size of chuncks to split the data into.

requiredNameTypeDescriptionDefault
args + Namespace +

positional and optional CLI arguments.

+ required +
+ +

Returns:

+ + - - - - + + - -
progressbool

default = None

TrueTypeDescription
+ + + + + str + +

path to edited torrent file.

+ + + +
- Source code in torrentfile\hasher.py -
class Hasher(CbMixin, ProgMixin):
+          Source code in torrentfile\commands.py
+          
121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
def edit(args: list):
     """
-    Piece hasher for Bittorrent V1 files.
-
-    Takes a sorted list of all file paths, calculates sha1 hash
-    for fixed size pieces of file data from each file
-    seemlessly until the last piece which may be smaller than others.
+    Execute the edit CLI sub-command with provided arguments.
 
     Parameters
     ----------
-    paths : list
-        List of files.
-    piece_length : int
-        Size of chuncks to split the data into.
-    progress : int
-        default = None
+    args : Namespace
+        positional and optional CLI arguments.
+
+    Returns
+    -------
+    str
+        path to edited torrent file.
     """
+    metafile = args.metafile
+    logger.info("Editing %s Meta File", str(args.metafile))
+    editargs = {
+        "url-list": args.url_list,
+        "httpseeds": args.httpseeds,
+        "announce": args.announce,
+        "source": args.source,
+        "private": args.private,
+        "comment": args.comment,
+    }
+    return edit_torrent(metafile, editargs)
+
+
+
+
- def __init__(self, paths: list, piece_length: int, progress: bool = True): - """Generate hashes of piece length data from filelist contents.""" - self.piece_length = piece_length - self.paths = paths - self.progress = progress - self.total = sum([os.path.getsize(i) for i in self.paths]) - self.index = 0 - self.current = open(self.paths[0], "rb") - if self.progress: - total = os.path.getsize(self.paths[0]) - self.prog_start(total, self.paths[0], unit="bytes") - logger.debug("Hashing %s", str(self.paths[0])) +
- def __iter__(self): - """ - Iterate through feed pieces. - Returns - ------- - self : iterator - Iterator for leaves/hash pieces. - """ - return self - def _handle_partial(self, arr: bytearray) -> bytearray: - """ - Define the handling partial pieces that span 2 or more files. +
- Parameters - ---------- - arr : bytearray - Incomplete piece containing partial data - Returns - ------- - digest : bytearray - SHA1 digest of the complete piece. - """ - while len(arr) < self.piece_length and self.next_file(): - target = self.piece_length - len(arr) - temp = bytearray(target) - size = self.current.readinto(temp) - arr.extend(temp[:size]) - self.prog_update(size) - if size == target: - break - return sha1(arr).digest() # nosec - def next_file(self) -> bool: - """ - Seemlessly transition to next file in file list. +

+info(args) - Returns - ------- - bool: - True if there is a next file otherwise False. - """ - self.index += 1 - self.prog_close() - if self.index < len(self.paths): - path = self.paths[self.index] - logger.debug("Hashing %s", str(path)) - self.current.close() - if self.progress: - self.prog_start(os.path.getsize(path), path, unit="bytes") - self.current = open(path, "rb") - return True - return False - def __next__(self) -> bytes: - """ - Generate piece-length pieces of data from input file list. +

- Returns - ------- - bytes - SHA1 hash of the piece extracted. - """ - while True: - piece = bytearray(self.piece_length) - size = self.current.readinto(piece) - if size == 0: - if not self.next_file(): - raise StopIteration - elif size < self.piece_length: - self.prog_update(size) - return self._handle_partial(piece[:size]) - else: - self.prog_update(size) - return sha1(piece).digest() # nosec + +
+ +

Show torrent metafile details to user via stdout.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + dict +

command line arguements provided by the user.

+ required +
+ +
+ Source code in torrentfile\commands.py +
 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
def info(args: list):
+    """
+    Show torrent metafile details to user via stdout.
+
+    Parameters
+    ----------
+    args : dict
+        command line arguements provided by the user.
+    """
+    metafile = args.metafile
+    meta = pyben.load(metafile)
+    info = meta["info"]
+    del meta["info"]
+    meta.update(info)
+    if "private" in meta and meta["private"] == 1:
+        meta["private"] = "True"
+    if "announce-list" in meta:
+        lst = meta["announce-list"]
+        meta["announce-list"] = ", ".join([j for i in lst for j in i])
+    if "url-list" in meta:
+        meta["url-list"] = ", ".join(meta["url-list"])
+    if "httpseeds" in meta:
+        meta["httpseeds"] = ", ".join(meta["httpseeds"])
+    text = []
+    longest = max([len(i) for i in meta.keys()])
+    for key, val in meta.items():
+        if key not in ["pieces", "piece layers", "files", "file tree"]:
+            prefix = longest - len(key) + 1
+            string = key + (" " * prefix) + str(val)
+            text.append(string)
+    most = max([len(i) for i in text])
+    text = ["-" * most, "\n"] + text + ["\n", "-" * most]
+    output = "\n".join(text)
+    print(output)
+    return output
 
+
+
+
-
+
+

+magnet(metafile) +

+
-
+

Create a magnet URI from a Bittorrent meta file.

+

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
metafile + (Namespace||str) +

Namespace class for CLI arguments.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

created magnet URI.

+
+ Source code in torrentfile\commands.py +
175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
def magnet(metafile):
+    """
+    Create a magnet URI from a Bittorrent meta file.
 
-
-__init__(self, paths, piece_length, progress=True) + Parameters + ---------- + metafile : (Namespace||str) + Namespace class for CLI arguments. - - special - + Returns + ------- + str + created magnet URI. + """ + if hasattr(metafile, "metafile"): + metafile = metafile.metafile + if not os.path.exists(metafile): + raise FileNotFoundError -
+ meta = pyben.load(metafile) + info = meta["info"] + binfo = pyben.dumps(info) + infohash = sha1(binfo).hexdigest().upper() # nosec -
+ logger.info("Magnet Info Hash: %s", infohash) + scheme = "magnet:" + hasharg = "?xt=urn:btih:" + infohash + namearg = "&dn=" + quote_plus(info["name"]) -

Generate hashes of piece length data from filelist contents.

+ if "announce-list" in meta: + announce_args = [ + "&tr=" + quote_plus(url) + for urllist in meta["announce-list"] + for url in urllist + ] + else: + announce_args = ["&tr=" + quote_plus(meta["announce"])] -
- Source code in torrentfile\hasher.py -
def __init__(self, paths: list, piece_length: int, progress: bool = True):
-    """Generate hashes of piece length data from filelist contents."""
-    self.piece_length = piece_length
-    self.paths = paths
-    self.progress = progress
-    self.total = sum([os.path.getsize(i) for i in self.paths])
-    self.index = 0
-    self.current = open(self.paths[0], "rb")
-    if self.progress:
-        total = os.path.getsize(self.paths[0])
-        self.prog_start(total, self.paths[0], unit="bytes")
-    logger.debug("Hashing %s", str(self.paths[0]))
+    full_uri = "".join([scheme, hasharg, namearg] + announce_args)
+    logger.info("Created Magnet URI %s", full_uri)
+    sys.stdout.write("\n" + full_uri + "\n")
+    return full_uri
 
+
@@ -3754,51 +4631,116 @@
-
+
-
-__iter__(self) +

+recheck(args) - - special - -

+
+
-

Iterate through feed pieces.

+

Execute recheck CLI sub-command.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + Namespace +

positional and optional arguments.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

The percentage of content currently saved to disk.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
iterator

Iterator for leaves/hash pieces.

- Source code in torrentfile\hasher.py -
def __iter__(self):
+          Source code in torrentfile\commands.py
+          
148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
def recheck(args):
     """
-    Iterate through feed pieces.
+    Execute recheck CLI sub-command.
+
+    Parameters
+    ----------
+    args : Namespace
+        positional and optional arguments.
 
     Returns
     -------
-    self : iterator
-        Iterator for leaves/hash pieces.
+    str
+        The percentage of content currently saved to disk.
     """
-    return self
+    metafile = args.metafile
+    content = args.content
+    logger.debug("Validating %s against %s contents", metafile, content)
+    checker = Checker(metafile, content)
+
+    logger.debug("Completed initialization of the Checker class")
+    result = checker.results()
+
+    sys.stdout.write(str(result) + "% Match\n")
+    sys.stdout.flush()
+    return result
 
+
@@ -3806,121 +4748,360 @@
-
+
-
-__next__(self) +
- - special - +
- -
-

Generate piece-length pieces of data from input file list.

+
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytes

SHA1 hash of the piece extracted.

-
- Source code in torrentfile\hasher.py -
def __next__(self) -> bytes:
-    """
-    Generate piece-length pieces of data from input file list.
 
-    Returns
-    -------
-    bytes
-        SHA1 hash of the piece extracted.
-    """
-    while True:
-        piece = bytearray(self.piece_length)
-        size = self.current.readinto(piece)
-        if size == 0:
-            if not self.next_file():
-                raise StopIteration
-        elif size < self.piece_length:
-            self.prog_update(size)
-            return self._handle_partial(piece[:size])
-        else:
-            self.prog_update(size)
-            return sha1(piece).digest()  # nosec
-
-
-
-
+

+ edit -
+

+
+

Edit torrent module.

+

Provides a facility by which certain properties of a torrent meta file can be +edited by the user. The various command line arguments indicate which fields +should be edited, and what the new value should be. Depending on what fields +are chosen to edit, this command can trigger a new info hash which means the +torrent will no longer be able to participate in the same swarm as the original +unedited torrent.

+
Keywords
+

private +comment +source +trackers +web-seeds

-
-next_file(self) -
+
-
-

Seemlessly transition to next file in file list.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bool

True if there is a next file otherwise False.

-
- Source code in torrentfile\hasher.py -
def next_file(self) -> bool:
-    """
-    Seemlessly transition to next file in file list.
+
+
+
+
+
+
+  
+ + + +

+edit_torrent(metafile, args) + + +

+ + +
+ +

Edit the properties and values in a torrent meta file.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
metafile + str +

path to the torrent meta file.

+ required +
args + dict +

key value pairs of the properties to be edited.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ dict +

The edited and nested Meta and info dictionaries.

+ +
+ Source code in torrentfile\edit.py +
 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
def edit_torrent(metafile: str, args: dict) -> dict:
+    """
+    Edit the properties and values in a torrent meta file.
+
+    Parameters
+    ----------
+    metafile : str
+        path to the torrent meta file.
+    args : dict
+        key value pairs of the properties to be edited.
 
     Returns
     -------
-    bool:
-        True if there is a next file otherwise False.
+    dict
+        The edited and nested Meta and info dictionaries.
     """
-    self.index += 1
-    self.prog_close()
-    if self.index < len(self.paths):
-        path = self.paths[self.index]
-        logger.debug("Hashing %s", str(path))
-        self.current.close()
-        if self.progress:
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
-        self.current = open(path, "rb")
-        return True
-    return False
+    logger.debug("editing torrent file %s", metafile)
+    meta = pyben.load(metafile)
+    info = meta["info"]
+    filter_empty(args, meta, info)
+
+    if "comment" in args:
+        info["comment"] = args["comment"]
+
+    if "source" in args:
+        info["source"] = args["source"]
+
+    if "private" in args:
+        info["private"] = 1
+
+    if "announce" in args:
+        val = args.get("announce", None)
+        if isinstance(val, str):
+            vallist = val.split()
+            meta["announce"] = vallist[0]
+            meta["announce-list"] = [vallist]
+        elif isinstance(val, list):
+            meta["announce"] = val[0]
+            meta["announce-list"] = [val]
+
+    if "url-list" in args:
+        val = args.get("url-list")
+        if isinstance(val, str):
+            meta["url-list"] = val.split()
+        elif isinstance(val, list):
+            meta["url-list"] = val
+
+    if "httpseeds" in args:
+        val = args.get("httpseeds")
+        if isinstance(val, str):
+            meta["httpseeds"] = val.split()
+        elif isinstance(val, list):
+            meta["httpseeds"] = val
+
+    meta["info"] = info
+    os.remove(metafile)
+    pyben.dump(meta, metafile)
+    return meta
+
+
+
+
+ +
+ + + +
+ + + +

+filter_empty(args, meta, info) + + +

+ + +
+ +

Remove dictionary keys with empty values.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + dict +

Editable metafile properties from user.

+ required +
meta + dict +

Metafile data dictionary.

+ required +
info + dict +

Metafile info dictionary.

+ required +
+ +
+ Source code in torrentfile\edit.py +
46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
def filter_empty(args: dict, meta: dict, info: dict):
+    """
+    Remove dictionary keys with empty values.
+
+    Parameters
+    ----------
+    args : dict
+        Editable metafile properties from user.
+    meta : dict
+        Metafile data dictionary.
+    info : dict
+        Metafile info dictionary.
+    """
+    for key, val in list(args.items()):
+        if val is None:
+            del args[key]
+            continue
+
+        if val == "":
+            if key in meta:
+                del meta[key]
+            elif key in info:
+                del info[key]
+            del args[key]
+            logger.debug("removeing empty fields %s", val)
 
+
@@ -3938,66 +5119,246 @@
+
+ + + +

+ hasher + + + +

+ +
+ +

Piece/File Hashers for Bittorrent meta file contents.

+ + + +
+ + + + + + + + + +
-

- -HasherHybrid (CbMixin, ProgMixin) - +

+ FileHasher

+
+

+ Bases: CbMixin, ProgMixin

+

Calculate root and piece hashes for creating hybrid torrent file.

-

DEPRECATED

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
pathstr

path to target file.

required
piece_lengthint

piece length for data chunks.

required
+ - - - - + + + + - -
progressbool

default = None

TrueNameTypeDescriptionDefault
-
- Source code in torrentfile\hasher.py -
class HasherHybrid(CbMixin, ProgMixin):
+    
+    
+        
+          path
+          
+                str
+          
+          

path to target file.

+ + required + + + + piece_length + + int + +

piece length for data chunks.

+ + required + + + + progress + + int + +

default = None

+ + True + + + + + + +
+ Source code in torrentfile\hasher.py +
400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
class FileHasher(CbMixin, ProgMixin):
     """
     Calculate root and piece hashes for creating hybrid torrent file.
 
-    **DEPRECATED**
-
     Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
     With a branching factor of 2, merge layer hashes until blocks equal
     piece_length bytes for the piece layer, and then the root hash.
@@ -4012,11 +5373,15 @@ 

default = None """ - def __init__(self, path: str, piece_length: int, progress: bool = True): + def __init__( + self, + path: str, + piece_length: int, + progress: bool = True, + hybrid: bool = False, + ): """ Construct Hasher class instances for each file in torrent. - - **DEPRECATED** """ self.path = path self.piece_length = piece_length @@ -4026,18 +5391,22 @@

self.root = None self.padding_piece = None self.padding_file = None + self.amount = piece_length // BLOCK_SIZE + self.end = False + self.current = open(path, "rb") + self.hybrid = hybrid if progress: + self.progressbar = True self.prog_start(os.path.getsize(path), path, unit="bytes") - self.amount = piece_length // BLOCK_SIZE - with open(path, "rb") as data: - self.process_file(data) + + def __iter__(self): + """Return `self`: needed to implement iterator implementation.""" + return self def _pad_remaining(self, block_count: int): """ Generate Hash sized, 0 filled bytes for padding. - **DEPRECATED** - Parameters ---------- block_count : int @@ -4053,60 +5422,67 @@

if not self.layer_hashes: power2 = next_power_2(block_count) remaining = power2 - block_count - self.prog_update(HASH_SIZE * remaining) return [bytes(HASH_SIZE) for _ in range(remaining)] - def process_file(self, data: bytearray): + def __next__(self) -> bytes: """ Calculate layer hashes for contents of file. - **DEPRECATED** - Parameters ---------- data : BytesIO File opened in read mode. + + Returns + ------- + bytes + The layer merckle root hash. """ - while True: - plength = self.piece_length - blocks = [] - piece = sha1() # nosec - total = 0 - block = bytearray(BLOCK_SIZE) - for _ in range(self.amount): - size = data.readinto(block) - self.prog_update(size) - if not size: - break - total += size - plength -= size - blocks.append(sha256(block[:size]).digest()) - piece.update(block[:size]) - if not blocks: + if self.end: + self.end = False + raise StopIteration + plength = self.piece_length + blocks = [] + piece = sha1() # nosec + total = 0 + block = bytearray(BLOCK_SIZE) + for _ in range(self.amount): + size = self.current.readinto(block) + if not size: + self.end = True break - if len(blocks) != self.amount: - padding = self._pad_remaining(len(blocks)) - blocks.extend(padding) - layer_hash = merkle_root(blocks) - if self._cb: - self._cb(layer_hash) - self.layer_hashes.append(layer_hash) - if plength > 0: + total += size + plength -= size + blocks.append(sha256(block[:size]).digest()) + if self.hybrid: + piece.update(block[:size]) + if len(blocks) != self.amount: + padding = self._pad_remaining(len(blocks)) + blocks.extend(padding) + self.prog_update(total) + layer_hash = merkle_root(blocks) + self.layer_hashes.append(layer_hash) + if self._cb: + self._cb(layer_hash) + if self.end: + self._calculate_root() + self.prog_close() + if self.hybrid: + if plength > 0: self.padding_file = { "attr": "p", "length": plength, "path": [".pad", str(plength)], } piece.update(bytes(plength)) - self.pieces.append(piece.digest()) # nosec - self._calculate_root() - self.prog_close() + piece = piece.digest() + self.pieces.append(piece) + return layer_hash, piece + return layer_hash def _calculate_root(self): """ Calculate the root hash for opened file. - - **DEPRECATED** """ self.piece_layer = b"".join(self.layer_hashes) @@ -4118,8 +5494,10 @@

self.layer_hashes += [pad_piece for _ in range(remainder)] self.root = merkle_root(self.layer_hashes) + self.current.close()

- +
+
@@ -4133,31 +5511,68 @@

-
-
-__init__(self, path, piece_length, progress=True) - - special - + + + + + + + + +
+ + + +
+__init__(path, piece_length, progress=True, hybrid=False) +
+

Construct Hasher class instances for each file in torrent.

-

DEPRECATED

Source code in torrentfile\hasher.py -
def __init__(self, path: str, piece_length: int, progress: bool = True):
+          
418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
def __init__(
+    self,
+    path: str,
+    piece_length: int,
+    progress: bool = True,
+    hybrid: bool = False,
+):
     """
     Construct Hasher class instances for each file in torrent.
-
-    **DEPRECATED**
     """
     self.path = path
     self.piece_length = piece_length
@@ -4167,12 +5582,46 @@ 
self.root = None self.padding_piece = None self.padding_file = None + self.amount = piece_length // BLOCK_SIZE + self.end = False + self.current = open(path, "rb") + self.hybrid = hybrid if progress: + self.progressbar = True self.prog_start(os.path.getsize(path), path, unit="bytes") - self.amount = piece_length // BLOCK_SIZE - with open(path, "rb") as data: - self.process_file(data)
+
+
+
+ +
+ + + +
+ + + +
+__iter__() + + +
+ + +
+ +

Return self: needed to implement iterator implementation.

+ +
+ Source code in torrentfile\hasher.py +
444
+445
+446
def __iter__(self):
+    """Return `self`: needed to implement iterator implementation."""
+    return self
+
+
@@ -4180,77 +5629,163 @@
-
+
-
-process_file(self, data) +
+__next__()
+

Calculate layer hashes for contents of file.

-

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
data + BytesIO +

File opened in read mode.

+ required +
+ +

Returns:

+ + - - - - + + - -
databytearray

File opened in read mode.

requiredTypeDescription
+ + + + + bytes + +

The layer merckle root hash.

+ + + +
Source code in torrentfile\hasher.py -
def process_file(self, data: bytearray):
+          
469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
def __next__(self) -> bytes:
     """
     Calculate layer hashes for contents of file.
 
-    **DEPRECATED**
-
     Parameters
     ----------
     data : BytesIO
         File opened in read mode.
+
+    Returns
+    -------
+    bytes
+        The layer merckle root hash.
     """
-    while True:
-        plength = self.piece_length
-        blocks = []
-        piece = sha1()  # nosec
-        total = 0
-        block = bytearray(BLOCK_SIZE)
-        for _ in range(self.amount):
-            size = data.readinto(block)
-            self.prog_update(size)
-            if not size:
-                break
-            total += size
-            plength -= size
-            blocks.append(sha256(block[:size]).digest())
-            piece.update(block[:size])
-        if not blocks:
+    if self.end:
+        self.end = False
+        raise StopIteration
+    plength = self.piece_length
+    blocks = []
+    piece = sha1()  # nosec
+    total = 0
+    block = bytearray(BLOCK_SIZE)
+    for _ in range(self.amount):
+        size = self.current.readinto(block)
+        if not size:
+            self.end = True
             break
-        if len(blocks) != self.amount:
-            padding = self._pad_remaining(len(blocks))
-            blocks.extend(padding)
-        layer_hash = merkle_root(blocks)
-        if self._cb:
-            self._cb(layer_hash)
-        self.layer_hashes.append(layer_hash)
+        total += size
+        plength -= size
+        blocks.append(sha256(block[:size]).digest())
+        if self.hybrid:
+            piece.update(block[:size])
+    if len(blocks) != self.amount:
+        padding = self._pad_remaining(len(blocks))
+        blocks.extend(padding)
+    self.prog_update(total)
+    layer_hash = merkle_root(blocks)
+    self.layer_hashes.append(layer_hash)
+    if self._cb:
+        self._cb(layer_hash)
+    if self.end:
+        self._calculate_root()
+        self.prog_close()
+    if self.hybrid:
         if plength > 0:
             self.padding_file = {
                 "attr": "p",
@@ -4258,10 +5793,12 @@ 
"path": [".pad", str(plength)], } piece.update(bytes(plength)) - self.pieces.append(piece.digest()) # nosec - self._calculate_root() - self.prog_close() + piece = piece.digest() + self.pieces.append(piece) + return layer_hash, piece + return layer_hash
+
@@ -4269,405 +5806,588 @@
+
+ -
+
+_calculate_root() + + +
+ + +
+ +

Calculate the root hash for opened file.

+ +
+ Source code in torrentfile\hasher.py +
525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
def _calculate_root(self):
+    """
+    Calculate the root hash for opened file.
+    """
+    self.piece_layer = b"".join(self.layer_hashes)
 
+    if len(self.layer_hashes) > 1:
+        pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
+
+        pow2 = next_power_2(len(self.layer_hashes))
+        remainder = pow2 - len(self.layer_hashes)
+
+        self.layer_hashes += [pad_piece for _ in range(remainder)]
+    self.root = merkle_root(self.layer_hashes)
+    self.current.close()
+
+
+
-
+
-

- -HasherV2 (CbMixin, ProgMixin) - +

+_pad_remaining(block_count) +
-
-

Calculate the root hash and piece layers for file contents.

-

DEPRECATED

-

Iterates over 16KiB blocks of data from given file, hashes the data, -then creates a hash tree from the individual block hashes until size of -hashed data equals the piece-length. Then continues the hash tree until -root hash is calculated.

+

Generate Hash sized, 0 filled bytes for padding.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
pathstr

Path to file.

required
+ - - - - + + + + + + + + + + + + + +
piece_lengthint

Size of layer hashes pieces.

requiredNameTypeDescriptionDefault
block_count + int +

current total number of blocks collected.

+ required +
+ +

Returns:

+ + - - - - + + - -
progressbool

default = None

TrueName TypeDescription
+ + + +padding + bytes + +

Padding to fill remaining portion of tree.

+ + + +
Source code in torrentfile\hasher.py -
class HasherV2(CbMixin, ProgMixin):
-    """
-    Calculate the root hash and piece layers for file contents.
-
-    **DEPRECATED**
-
-    Iterates over 16KiB blocks of data from given file, hashes the data,
-    then creates a hash tree from the individual block hashes until size of
-    hashed data equals the piece-length.  Then continues the hash tree until
-    root hash is calculated.
+          
448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
def _pad_remaining(self, block_count: int):
+    """
+    Generate Hash sized, 0 filled bytes for padding.
 
     Parameters
     ----------
-    path : str
-        Path to file.
-    piece_length : int
-        Size of layer hashes pieces.
-    progress : int
-        default = None
+    block_count : int
+        current total number of blocks collected.
+
+    Returns
+    -------
+    padding : bytes
+        Padding to fill remaining portion of tree.
     """
+    # when the there is only one block for file
+    remaining = self.amount - block_count
+    if not self.layer_hashes:
+        power2 = next_power_2(block_count)
+        remaining = power2 - block_count
+    return [bytes(HASH_SIZE) for _ in range(remaining)]
+
+
+
+
- def __init__(self, path: str, piece_length: int, progress: bool = True): - """ - Calculate and store hash information for specific file. +
- **DEPRECATED** - """ - self.path = path - self.root = None - self.piece_layer = None - self.layer_hashes = [] - self.piece_length = piece_length - self.num_blocks = piece_length // BLOCK_SIZE - if progress: - self.prog_start(os.path.getsize(path), path, unit="bytes") - with open(self.path, "rb") as fd: - self.process_file(fd) - def process_file(self, fd: str): - """ - Calculate hashes over 16KiB chuncks of file content. - **DEPRECATED** - Parameters - ---------- - fd : TextIOWrapper - Opened file in read mode. - """ - while True: - blocks = [] - leaf = bytearray(BLOCK_SIZE) - # generate leaves of merkle tree - for _ in range(self.num_blocks): - size = fd.readinto(leaf) - self.prog_update(size) - if not size: - break - blocks.append(sha256(leaf[:size]).digest()) +
- # blocks is empty mean eof - if not blocks: - break - if len(blocks) != self.num_blocks: - # when size of file doesn't fill the last block - # when the file contains multiple pieces - remaining = self.num_blocks - len(blocks) - if not self.layer_hashes: - # when the there is only one block for file - power2 = next_power_2(len(blocks)) - remaining = power2 - len(blocks) +

- # pad the the rest with zeroes to fill remaining space. - padding = [bytes(32) for _ in range(remaining)] - self.prog_update(HASH_SIZE * remaining) - blocks.extend(padding) - # calculate the root hash for the merkle tree up to piece-length +
- layer_hash = merkle_root(blocks) - if self._cb: - self._cb(layer_hash) - self.layer_hashes.append(layer_hash) - self._calculate_root() - self.prog_close() - def _calculate_root(self): - """ - Calculate root hash for the target file. - **DEPRECATED** - """ - self.piece_layer = b"".join(self.layer_hashes) - hashes = len(self.layer_hashes) - if hashes > 1: - pow2 = next_power_2(hashes) - remainder = pow2 - hashes - pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)] - for _ in range(remainder): - self.layer_hashes.append(merkle_root(pad_piece)) - self.root = merkle_root(self.layer_hashes) -
-
+
-
+

+ Hasher +

+
+

+ Bases: CbMixin, ProgMixin

+

Piece hasher for Bittorrent V1 files.

+

Takes a sorted list of all file paths, calculates sha1 hash +for fixed size pieces of file data from each file +seemlessly until the last piece which may be smaller than others.

+

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
paths + list +

List of files.

+ required +
piece_length + int +

Size of chuncks to split the data into.

+ required +
progress + int +

default = None

+ True +
+ + +
+ Source code in torrentfile\hasher.py +
 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
class Hasher(CbMixin, ProgMixin):
+    """
+    Piece hasher for Bittorrent V1 files.
 
-  
+ Takes a sorted list of all file paths, calculates sha1 hash + for fixed size pieces of file data from each file + seemlessly until the last piece which may be smaller than others. + Parameters + ---------- + paths : list + List of files. + piece_length : int + Size of chuncks to split the data into. + progress : int + default = None + """ + def __init__(self, paths: list, piece_length: int, progress: bool = True): + """Generate hashes of piece length data from filelist contents.""" + self.piece_length = piece_length + self.paths = paths + self.progress = progress + self.total = sum([os.path.getsize(i) for i in self.paths]) + self.index = 0 + self.current = open(self.paths[0], "rb") + if self.progress: + total = os.path.getsize(self.paths[0]) + self.prog_start(total, self.paths[0], unit="bytes") + logger.debug("Hashing %s", str(self.paths[0])) -
-__init__(self, path, piece_length, progress=True) + def __iter__(self): + """ + Iterate through feed pieces. - - special - + Returns + ------- + self : iterator + Iterator for leaves/hash pieces. + """ + return self -
+ def _handle_partial(self, arr: bytearray) -> bytearray: + """ + Define the handling partial pieces that span 2 or more files. -
+ Parameters + ---------- + arr : bytearray + Incomplete piece containing partial data -

Calculate and store hash information for specific file.

-

DEPRECATED

+ Returns + ------- + digest : bytearray + SHA1 digest of the complete piece. + """ + while len(arr) < self.piece_length and self.next_file(): + target = self.piece_length - len(arr) + temp = bytearray(target) + size = self.current.readinto(temp) + arr.extend(temp[:size]) + self.prog_update(size) + if size == target: + break + return sha1(arr).digest() # nosec -
- Source code in torrentfile\hasher.py -
def __init__(self, path: str, piece_length: int, progress: bool = True):
-    """
-    Calculate and store hash information for specific file.
+    def next_file(self) -> bool:
+        """
+        Seemlessly transition to next file in file list.
 
-    **DEPRECATED**
-    """
-    self.path = path
-    self.root = None
-    self.piece_layer = None
-    self.layer_hashes = []
-    self.piece_length = piece_length
-    self.num_blocks = piece_length // BLOCK_SIZE
-    if progress:
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
-    with open(self.path, "rb") as fd:
-        self.process_file(fd)
+        Returns
+        -------
+        bool:
+            True if there is a next file otherwise False.
+        """
+        self.index += 1
+        self.prog_close()
+        if self.index < len(self.paths):
+            path = self.paths[self.index]
+            logger.debug("Hashing %s", str(path))
+            self.current.close()
+            if self.progress:
+                self.prog_start(os.path.getsize(path), path, unit="bytes")
+            self.current = open(path, "rb")
+            return True
+        return False
+
+    def __next__(self) -> bytes:
+        """
+        Generate piece-length pieces of data from input file list.
+
+        Returns
+        -------
+        bytes
+            SHA1 hash of the piece extracted.
+        """
+        while True:
+            piece = bytearray(self.piece_length)
+            size = self.current.readinto(piece)
+            if size == 0:
+                if not self.next_file():
+                    raise StopIteration
+            elif size < self.piece_length:
+                self.prog_update(size)
+                return self._handle_partial(piece[:size])
+            else:
+                self.prog_update(size)
+                return sha1(piece).digest()  # nosec
 
-
-
+
+
-
+
-
-
-process_file(self, fd) -
-
-

Calculate hashes over 16KiB chuncks of file content.

-

DEPRECATED

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
fdstr

Opened file in read mode.

required
-
- Source code in torrentfile\hasher.py -
def process_file(self, fd: str):
-    """
-    Calculate hashes over 16KiB chuncks of file content.
 
-    **DEPRECATED**
 
-    Parameters
-    ----------
-    fd : TextIOWrapper
-        Opened file in read mode.
-    """
-    while True:
-        blocks = []
-        leaf = bytearray(BLOCK_SIZE)
-        # generate leaves of merkle tree
 
-        for _ in range(self.num_blocks):
-            size = fd.readinto(leaf)
-            self.prog_update(size)
-            if not size:
-                break
-            blocks.append(sha256(leaf[:size]).digest())
 
-        # blocks is empty mean eof
-        if not blocks:
-            break
-        if len(blocks) != self.num_blocks:
-            # when size of file doesn't fill the last block
-            # when the file contains multiple pieces
-            remaining = self.num_blocks - len(blocks)
-            if not self.layer_hashes:
-                # when the there is only one block for file
-                power2 = next_power_2(len(blocks))
-                remaining = power2 - len(blocks)
 
-            # pad the the rest with zeroes to fill remaining space.
-            padding = [bytes(32) for _ in range(remaining)]
-            self.prog_update(HASH_SIZE * remaining)
-            blocks.extend(padding)
-        # calculate the root hash for the merkle tree up to piece-length
+  
- layer_hash = merkle_root(blocks) - if self._cb: - self._cb(layer_hash) - self.layer_hashes.append(layer_hash) - self._calculate_root() - self.prog_close() -
-
-
-
+
+__init__(paths, piece_length, progress=True) +
-
+
+

Generate hashes of piece length data from filelist contents.

+ +
+ Source code in torrentfile\hasher.py +
54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
def __init__(self, paths: list, piece_length: int, progress: bool = True):
+    """Generate hashes of piece length data from filelist contents."""
+    self.piece_length = piece_length
+    self.paths = paths
+    self.progress = progress
+    self.total = sum([os.path.getsize(i) for i in self.paths])
+    self.index = 0
+    self.current = open(self.paths[0], "rb")
+    if self.progress:
+        total = os.path.getsize(self.paths[0])
+        self.prog_start(total, self.paths[0], unit="bytes")
+    logger.debug("Hashing %s", str(self.paths[0]))
+
+
+
-
-

-merkle_root(blocks) +

+__iter__() -
+ +
-

Calculate the merkle root for a seq of sha256 hash digests.

+

Iterate through feed pieces.

-

Parameters:

- - - - - - - - - - +

Returns:

+
NameTypeDescriptionDefault
+ - - - - + + - -
blockslist

a sequence of sha256 layer hashes.

requiredName TypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytes

the sha256 root hash of the merkle tree.

+ + + +self + iterator + +

Iterator for leaves/hash pieces.

+ + + +
Source code in torrentfile\hasher.py -
def merkle_root(blocks: list) -> bytes:
+          
67
+68
+69
+70
+71
+72
+73
+74
+75
+76
def __iter__(self):
     """
-    Calculate the merkle root for a seq of sha256 hash digests.
-
-    Parameters
-    ----------
-    blocks : list
-        a sequence of sha256 layer hashes.
+    Iterate through feed pieces.
 
     Returns
     -------
-    bytes
-        the sha256 root hash of the merkle tree.
+    self : iterator
+        Iterator for leaves/hash pieces.
     """
-    if blocks:
-        while len(blocks) > 1:
-            blocks = [
-                sha256(x + y).digest() for x, y in zip(*[iter(blocks)] * 2)
-            ]
-        return blocks[0]
-    return blocks
+    return self
 
+
@@ -4675,574 +6395,757 @@

+
-
- -

- -
- - - -
- - - -

- interactive +

+__next__() +
-
-

Module contains the procedures used for Interactive Mode.

- - - -
+

Generate piece-length pieces of data from input file list.

+

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ bytes +

SHA1 hash of the piece extracted.

+
+ Source code in torrentfile\hasher.py +
123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
def __next__(self) -> bytes:
+    """
+    Generate piece-length pieces of data from input file list.
 
+    Returns
+    -------
+    bytes
+        SHA1 hash of the piece extracted.
+    """
+    while True:
+        piece = bytearray(self.piece_length)
+        size = self.current.readinto(piece)
+        if size == 0:
+            if not self.next_file():
+                raise StopIteration
+        elif size < self.piece_length:
+            self.prog_update(size)
+            return self._handle_partial(piece[:size])
+        else:
+            self.prog_update(size)
+            return sha1(piece).digest()  # nosec
+
+
+
+
+
-
+
-

- -InteractiveCreator +

+_handle_partial(arr) +
-
-

Class namespace for interactive program options.

+

Define the handling partial pieces that span 2 or more files.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
arr + bytearray +

Incomplete piece containing partial data

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
digest + bytearray +

SHA1 digest of the complete piece.

- Source code in torrentfile\interactive.py -
class InteractiveCreator:
-    """
-    Class namespace for interactive program options.
-    """
+          Source code in torrentfile\hasher.py
+          
 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
def _handle_partial(self, arr: bytearray) -> bytearray:
+    """
+    Define the handling partial pieces that span 2 or more files.
 
-    def __init__(self):
-        """
-        Initialize interactive meta file creator dialog.
-        """
-        self.kwargs = {
-            "announce": None,
-            "url_list": None,
-            "private": None,
-            "source": None,
-            "comment": None,
-            "piece_length": None,
-            "outfile": None,
-            "path": None,
-            "httpseeds": None,
-        }
-        self.outfile, self.meta = self.get_props()
+    Parameters
+    ----------
+    arr : bytearray
+        Incomplete piece containing partial data
 
-    def get_props(self):
-        """
-        Gather details for torrentfile from user.
-        """
-        piece_length = get_input(
-            "Piece Length (empty=auto): ", lambda x: x.isdigit()
-        )
+    Returns
+    -------
+    digest : bytearray
+        SHA1 digest of the complete piece.
+    """
+    while len(arr) < self.piece_length and self.next_file():
+        target = self.piece_length - len(arr)
+        temp = bytearray(target)
+        size = self.current.readinto(temp)
+        arr.extend(temp[:size])
+        self.prog_update(size)
+        if size == target:
+            break
+    return sha1(arr).digest()  # nosec
+
+
+
+
- self.kwargs["piece_length"] = piece_length - announce = get_input( - "Tracker list (empty): ", lambda x: isinstance(x, str) - ) +
- if announce: - self.kwargs["announce"] = announce.split() - url_list = get_input( - "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str) - ) - httpseeds = get_input( - "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str) - ) +
- if url_list: - self.kwargs["url_list"] = url_list.split() - if httpseeds: - self.kwargs["httpseeds"] = httpseeds.split() - comment = get_input("Comment (empty): ", None) - if comment: - self.kwargs["comment"] = comment - source = get_input("Source (empty): ", None) - if source: - self.kwargs["source"] = source +
+next_file() - private = get_input( - "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN" - ) - if private and private.lower() == "y": - self.kwargs["private"] = 1 +
- contents = get_input("Content Path: ", os.path.exists) - self.kwargs["path"] = contents - outfile = get_input( - f"Output Path ({contents}.torrent): ", - lambda x: os.path.exists(os.path.dirname(x)), - ) +
- if outfile: - self.kwargs["outfile"] = outfile +

Seemlessly transition to next file in file list.

- meta_version = get_input( - "Meta Version {1,2,3}: (1)", lambda x: x in "123" - ) +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
bool + bool +

True if there is a next file otherwise False.

- showcenter(f"creating {outfile}") +
+ Source code in torrentfile\hasher.py +
102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
def next_file(self) -> bool:
+    """
+    Seemlessly transition to next file in file list.
 
-        if meta_version == "3":
-            torrent = TorrentFileHybrid(**self.kwargs)
-        elif meta_version == "2":
-            torrent = TorrentFileV2(**self.kwargs)
-        else:
-            torrent = TorrentFile(**self.kwargs)
-        return torrent.write()
+    Returns
+    -------
+    bool:
+        True if there is a next file otherwise False.
+    """
+    self.index += 1
+    self.prog_close()
+    if self.index < len(self.paths):
+        path = self.paths[self.index]
+        logger.debug("Hashing %s", str(path))
+        self.current.close()
+        if self.progress:
+            self.prog_start(os.path.getsize(path), path, unit="bytes")
+        self.current = open(path, "rb")
+        return True
+    return False
 
+
+
- - -
+
+
+
+
-
+
-
-__init__(self) - - special - +

+ HasherHybrid -

-
-

Initialize interactive meta file creator dialog.

+ -
- Source code in torrentfile\interactive.py -
def __init__(self):
-    """
-    Initialize interactive meta file creator dialog.
-    """
-    self.kwargs = {
-        "announce": None,
-        "url_list": None,
-        "private": None,
-        "source": None,
-        "comment": None,
-        "piece_length": None,
-        "outfile": None,
-        "path": None,
-        "httpseeds": None,
-    }
-    self.outfile, self.meta = self.get_props()
-
-
-
-
+
+

+ Bases: CbMixin, ProgMixin

+

Calculate root and piece hashes for creating hybrid torrent file.

+

DEPRECATED

+

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. +With a branching factor of 2, merge layer hashes until blocks equal +piece_length bytes for the piece layer, and then the root hash.

-
+

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

path to target file.

+ required +
piece_length + int +

piece length for data chunks.

+ required +
progress + int +

default = None

+ True +
+ + +
+ Source code in torrentfile\hasher.py +
272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
class HasherHybrid(CbMixin, ProgMixin):
+    """
+    Calculate root and piece hashes for creating hybrid torrent file.
 
+    **DEPRECATED**
 
+    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
+    With a branching factor of 2, merge layer hashes until blocks equal
+    piece_length bytes for the piece layer, and then the root hash.
 
-
-get_props(self) + Parameters + ---------- + path : str + path to target file. + piece_length : int + piece length for data chunks. + progress : int + default = None + """ + def __init__(self, path: str, piece_length: int, progress: bool = True): + """ + Construct Hasher class instances for each file in torrent. -
+ **DEPRECATED** + """ + self.path = path + self.piece_length = piece_length + self.pieces = [] + self.layer_hashes = [] + self.piece_layer = None + self.root = None + self.padding_piece = None + self.padding_file = None + if progress: + self.prog_start(os.path.getsize(path), path, unit="bytes") + self.amount = piece_length // BLOCK_SIZE + with open(path, "rb") as data: + self.process_file(data) -
+ def _pad_remaining(self, block_count: int): + """ + Generate Hash sized, 0 filled bytes for padding. -

Gather details for torrentfile from user.

+ **DEPRECATED** -
- Source code in torrentfile\interactive.py -
def get_props(self):
-    """
-    Gather details for torrentfile from user.
-    """
-    piece_length = get_input(
-        "Piece Length (empty=auto): ", lambda x: x.isdigit()
-    )
+        Parameters
+        ----------
+        block_count : int
+            current total number of blocks collected.
 
-    self.kwargs["piece_length"] = piece_length
-    announce = get_input(
-        "Tracker list (empty): ", lambda x: isinstance(x, str)
-    )
+        Returns
+        -------
+        padding : bytes
+            Padding to fill remaining portion of tree.
+        """
+        # when the there is only one block for file
+        remaining = self.amount - block_count
+        if not self.layer_hashes:
+            power2 = next_power_2(block_count)
+            remaining = power2 - block_count
+        self.prog_update(HASH_SIZE * remaining)
+        return [bytes(HASH_SIZE) for _ in range(remaining)]
 
-    if announce:
-        self.kwargs["announce"] = announce.split()
+    def process_file(self, data: bytearray):
+        """
+        Calculate layer hashes for contents of file.
 
-    url_list = get_input(
-        "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str)
-    )
+        **DEPRECATED**
 
-    httpseeds = get_input(
-        "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str)
-    )
+        Parameters
+        ----------
+        data : BytesIO
+            File opened in read mode.
+        """
+        while True:
+            plength = self.piece_length
+            blocks = []
+            piece = sha1()  # nosec
+            total = 0
+            block = bytearray(BLOCK_SIZE)
+            for _ in range(self.amount):
+                size = data.readinto(block)
+                self.prog_update(size)
+                if not size:
+                    break
+                total += size
+                plength -= size
+                blocks.append(sha256(block[:size]).digest())
+                piece.update(block[:size])
+            if not blocks:
+                break
+            if len(blocks) != self.amount:
+                padding = self._pad_remaining(len(blocks))
+                blocks.extend(padding)
+            layer_hash = merkle_root(blocks)
+            if self._cb:
+                self._cb(layer_hash)
+            self.layer_hashes.append(layer_hash)
+            if plength > 0:
+                self.padding_file = {
+                    "attr": "p",
+                    "length": plength,
+                    "path": [".pad", str(plength)],
+                }
+                piece.update(bytes(plength))
+            self.pieces.append(piece.digest())  # nosec
+        self._calculate_root()
+        self.prog_close()
 
-    if url_list:
-        self.kwargs["url_list"] = url_list.split()
-    if httpseeds:
-        self.kwargs["httpseeds"] = httpseeds.split()
-    comment = get_input("Comment (empty): ", None)
+    def _calculate_root(self):
+        """
+        Calculate the root hash for opened file.
 
-    if comment:
-        self.kwargs["comment"] = comment
-    source = get_input("Source (empty): ", None)
+        **DEPRECATED**
+        """
+        self.piece_layer = b"".join(self.layer_hashes)
 
-    if source:
-        self.kwargs["source"] = source
+        if len(self.layer_hashes) > 1:
+            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
 
-    private = get_input(
-        "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN"
-    )
+            pow2 = next_power_2(len(self.layer_hashes))
+            remainder = pow2 - len(self.layer_hashes)
 
-    if private and private.lower() == "y":
-        self.kwargs["private"] = 1
+            self.layer_hashes += [pad_piece for _ in range(remainder)]
+        self.root = merkle_root(self.layer_hashes)
+
+
+
- contents = get_input("Content Path: ", os.path.exists) - self.kwargs["path"] = contents - outfile = get_input( - f"Output Path ({contents}.torrent): ", - lambda x: os.path.exists(os.path.dirname(x)), - ) - if outfile: - self.kwargs["outfile"] = outfile +
- meta_version = get_input( - "Meta Version {1,2,3}: (1)", lambda x: x in "123" - ) - showcenter(f"creating {outfile}") - if meta_version == "3": - torrent = TorrentFileHybrid(**self.kwargs) - elif meta_version == "2": - torrent = TorrentFileV2(**self.kwargs) - else: - torrent = TorrentFile(**self.kwargs) - return torrent.write() -
- -
-
-
-
-
-
-

- -InteractiveEditor +
-

+
+__init__(path, piece_length, progress=True) + + +
+
-

Interactive dialog class for torrent editing.

+

Construct Hasher class instances for each file in torrent.

+

DEPRECATED

- Source code in torrentfile\interactive.py -
class InteractiveEditor:
+          Source code in torrentfile\hasher.py
+          
292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
def __init__(self, path: str, piece_length: int, progress: bool = True):
     """
-    Interactive dialog class for torrent editing.
+    Construct Hasher class instances for each file in torrent.
+
+    **DEPRECATED**
     """
+    self.path = path
+    self.piece_length = piece_length
+    self.pieces = []
+    self.layer_hashes = []
+    self.piece_layer = None
+    self.root = None
+    self.padding_piece = None
+    self.padding_file = None
+    if progress:
+        self.prog_start(os.path.getsize(path), path, unit="bytes")
+    self.amount = piece_length // BLOCK_SIZE
+    with open(path, "rb") as data:
+        self.process_file(data)
+
+
+
+
- def __init__(self, metafile: str): - """ - Initialize the Interactive torrent editor guide. +
- Parameters - ---------- - metafile : str - user input string identifying the path to a torrent meta file. - """ - self.metafile = metafile - self.meta = pyben.load(metafile) - self.info = self.meta["info"] - self.args = { - "url-list": self.meta.get("url-list", None), - "httpseeds": self.meta.get("httpseeds", None), - "announce": self.meta.get("announce-list", None), - "source": self.info.get("source", None), - "private": self.info.get("private", None), - "comment": self.info.get("comment", None), - } - def show_current(self): - """ - Display the current met file information to screen. - """ - out = "Current properties and values:\n" - longest = max([len(label) for label in self.args]) + 3 - for key, val in self.args.items(): - txt = (key.title() + ":").ljust(longest) + str(val) - out += f"\t{txt}\n" - showtext(out) +
- def sanatize_response(self, key, response): - """ - Convert the input data into a form recognizable by the program. - Parameters - ---------- - key : str - name of the property and attribute being eddited. - response : str - User input value the property is being edited to. - """ - if key in ["announce", "url-list", "httpseeds"]: - val = response.split() - else: - val = response - self.args[key] = val - def edit_props(self): - """ - Loop continuosly for edits until user signals DONE. - """ - while True: - showcenter( - "Choose the number for a propert the needs editing." - "Enter DONE when all editing has been completed." - ) +
+_calculate_root() - props = { - 1: "comment", - 2: "source", - 3: "private", - 4: "tracker", - 5: "web-seed", - 6: "httpseeds", - } - - args = { - 1: "comment", - 2: "source", - 3: "private", - 4: "announce", - 5: "url-list", - 6: "httpseeds", - } - - txt = ", ".join((str(k) + ": " + v) for k, v in props.items()) - prop = get_input(txt) - if prop.lower() == "done": - break - - if prop.isdigit() and 0 < int(prop) < 6: - key = props[int(prop)] - key2 = args[int(prop)] - val = self.args.get(key2) - showtext( - "Enter new property value or leave empty for no value." - ) - response = get_input(f"{key.title()} ({val}): ") - self.sanatize_response(key2, response) - - else: - showtext("Invalid input: Try again.") - edit_torrent(self.metafile, self.args) -
- - - - -
- - - - - - - - - -
- - - -
-__init__(self, metafile) - - - special -
-
- -

Initialize the Interactive torrent editor guide.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
metafilestr

user input string identifying the path to a torrent meta file.

required
-
- Source code in torrentfile\interactive.py -
def __init__(self, metafile: str):
-    """
-    Initialize the Interactive torrent editor guide.
-
-    Parameters
-    ----------
-    metafile : str
-        user input string identifying the path to a torrent meta file.
-    """
-    self.metafile = metafile
-    self.meta = pyben.load(metafile)
-    self.info = self.meta["info"]
-
-    self.args = {
-        "url-list": self.meta.get("url-list", None),
-        "httpseeds": self.meta.get("httpseeds", None),
-        "announce": self.meta.get("announce-list", None),
-        "source": self.info.get("source", None),
-        "private": self.info.get("private", None),
-        "comment": self.info.get("comment", None),
-    }
-
-
-
- -
- - - -
- - - -
-edit_props(self) - - -
-

Loop continuosly for edits until user signals DONE.

+

Calculate the root hash for opened file.

+

DEPRECATED

- Source code in torrentfile\interactive.py -
def edit_props(self):
-    """
-    Loop continuosly for edits until user signals DONE.
-    """
-    while True:
-        showcenter(
-            "Choose the number for a propert the needs editing."
-            "Enter DONE when all editing has been completed."
-        )
-
-        props = {
-            1: "comment",
-            2: "source",
-            3: "private",
-            4: "tracker",
-            5: "web-seed",
-            6: "httpseeds",
-        }
+          Source code in torrentfile\hasher.py
+          
382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
def _calculate_root(self):
+    """
+    Calculate the root hash for opened file.
 
-        args = {
-            1: "comment",
-            2: "source",
-            3: "private",
-            4: "announce",
-            5: "url-list",
-            6: "httpseeds",
-        }
+    **DEPRECATED**
+    """
+    self.piece_layer = b"".join(self.layer_hashes)
 
-        txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
-        prop = get_input(txt)
-        if prop.lower() == "done":
-            break
+    if len(self.layer_hashes) > 1:
+        pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
 
-        if prop.isdigit() and 0 < int(prop) < 6:
-            key = props[int(prop)]
-            key2 = args[int(prop)]
-            val = self.args.get(key2)
-            showtext(
-                "Enter new property value or leave empty for no value."
-            )
-            response = get_input(f"{key.title()} ({val}): ")
-            self.sanatize_response(key2, response)
+        pow2 = next_power_2(len(self.layer_hashes))
+        remainder = pow2 - len(self.layer_hashes)
 
-        else:
-            showtext("Invalid input: Try again.")
-    edit_torrent(self.metafile, self.args)
+        self.layer_hashes += [pad_piece for _ in range(remainder)]
+    self.root = merkle_root(self.layer_hashes)
 
+
@@ -5250,64 +7153,113 @@
+
-
-sanatize_response(self, key, response) +
+_pad_remaining(block_count)
+
-

Convert the input data into a form recognizable by the program.

+

Generate Hash sized, 0 filled bytes for padding.

+

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + + + + + + + + + + +
keystr

name of the property and attribute being eddited.

requiredNameTypeDescriptionDefault
block_count + int +

current total number of blocks collected.

+ required +
+ +

Returns:

+ + - - - - + + - -
responsestr

User input value the property is being edited to.

requiredName TypeDescription
+ + + +padding + bytes + +

Padding to fill remaining portion of tree.

+ + + +
- Source code in torrentfile\interactive.py -
def sanatize_response(self, key, response):
-    """
-    Convert the input data into a form recognizable by the program.
+          Source code in torrentfile\hasher.py
+          
312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
def _pad_remaining(self, block_count: int):
+    """
+    Generate Hash sized, 0 filled bytes for padding.
+
+    **DEPRECATED**
 
     Parameters
     ----------
-    key : str
-        name of the property and attribute being eddited.
-    response : str
-        User input value the property is being edited to.
+    block_count : int
+        current total number of blocks collected.
+
+    Returns
+    -------
+    padding : bytes
+        Padding to fill remaining portion of tree.
     """
-    if key in ["announce", "url-list", "httpseeds"]:
-        val = response.split()
-    else:
-        val = response
-    self.args[key] = val
+    # when the there is only one block for file
+    remaining = self.amount - block_count
+    if not self.layer_hashes:
+        power2 = next_power_2(block_count)
+        remaining = power2 - block_count
+    self.prog_update(HASH_SIZE * remaining)
+    return [bytes(HASH_SIZE) for _ in range(remaining)]
 
+
@@ -5315,33 +7267,139 @@
+
-
-show_current(self) +
+process_file(data)
+
-

Display the current met file information to screen.

+

Calculate layer hashes for contents of file.

+

DEPRECATED

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
data + BytesIO +

File opened in read mode.

+ required +
- Source code in torrentfile\interactive.py -
def show_current(self):
+          Source code in torrentfile\hasher.py
+          
336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
def process_file(self, data: bytearray):
     """
-    Display the current met file information to screen.
+    Calculate layer hashes for contents of file.
+
+    **DEPRECATED**
+
+    Parameters
+    ----------
+    data : BytesIO
+        File opened in read mode.
     """
-    out = "Current properties and values:\n"
-    longest = max([len(label) for label in self.args]) + 3
-    for key, val in self.args.items():
-        txt = (key.title() + ":").ljust(longest) + str(val)
-        out += f"\t{txt}\n"
-    showtext(out)
+    while True:
+        plength = self.piece_length
+        blocks = []
+        piece = sha1()  # nosec
+        total = 0
+        block = bytearray(BLOCK_SIZE)
+        for _ in range(self.amount):
+            size = data.readinto(block)
+            self.prog_update(size)
+            if not size:
+                break
+            total += size
+            plength -= size
+            blocks.append(sha256(block[:size]).digest())
+            piece.update(block[:size])
+        if not blocks:
+            break
+        if len(blocks) != self.amount:
+            padding = self._pad_remaining(len(blocks))
+            blocks.extend(padding)
+        layer_hash = merkle_root(blocks)
+        if self._cb:
+            self._cb(layer_hash)
+        self.layer_hashes.append(layer_hash)
+        if plength > 0:
+            self.padding_file = {
+                "attr": "p",
+                "length": plength,
+                "path": [".pad", str(plength)],
+            }
+            piece.update(bytes(plength))
+        self.pieces.append(piece.digest())  # nosec
+    self._calculate_root()
+    self.prog_close()
 
+
@@ -5359,150 +7417,296 @@
-
+

+ HasherV2 -

-create_torrent()

+
+

+ Bases: CbMixin, ProgMixin

-

Create new torrent file interactively.

-
- Source code in torrentfile\interactive.py -
def create_torrent():
+      

Calculate the root hash and piece layers for file contents.

+

DEPRECATED

+

Iterates over 16KiB blocks of data from given file, hashes the data, +then creates a hash tree from the individual block hashes until size of +hashed data equals the piece-length. Then continues the hash tree until +root hash is calculated.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

Path to file.

+ required +
piece_length + int +

Size of layer hashes pieces.

+ required +
progress + int +

default = None

+ True +
+ + +
+ Source code in torrentfile\hasher.py +
169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
class HasherV2(CbMixin, ProgMixin):
     """
-    Create new torrent file interactively.
-    """
-    showcenter("Create Torrent")
-    showtext(
-        "\nEnter values for each of the options for the torrent creator, "
-        "or leave blank for program defaults.\nSpaces are considered item "
-        "seperators for options that accept a list of values.\nValues "
-        "enclosed in () indicate the default value, while {} holds all "
-        "valid choices available for the option.\n\n"
-    )
-    creator = InteractiveCreator()
-    return creator
-
- - + Calculate the root hash and piece layers for file contents. - + **DEPRECATED** + Iterates over 16KiB blocks of data from given file, hashes the data, + then creates a hash tree from the individual block hashes until size of + hashed data equals the piece-length. Then continues the hash tree until + root hash is calculated. + Parameters + ---------- + path : str + Path to file. + piece_length : int + Size of layer hashes pieces. + progress : int + default = None + """ -
+ def __init__(self, path: str, piece_length: int, progress: bool = True): + """ + Calculate and store hash information for specific file. + **DEPRECATED** + """ + self.path = path + self.root = None + self.piece_layer = None + self.layer_hashes = [] + self.piece_length = piece_length + self.num_blocks = piece_length // BLOCK_SIZE + if progress: + self.prog_start(os.path.getsize(path), path, unit="bytes") + with open(self.path, "rb") as fd: + self.process_file(fd) + def process_file(self, fd: str): + """ + Calculate hashes over 16KiB chuncks of file content. -

-edit_action() + **DEPRECATED** + Parameters + ---------- + fd : TextIOWrapper + Opened file in read mode. + """ + while True: + blocks = [] + leaf = bytearray(BLOCK_SIZE) + # generate leaves of merkle tree -

+ for _ in range(self.num_blocks): + size = fd.readinto(leaf) + self.prog_update(size) + if not size: + break + blocks.append(sha256(leaf[:size]).digest()) -
+ # blocks is empty mean eof + if not blocks: + break + if len(blocks) != self.num_blocks: + # when size of file doesn't fill the last block + # when the file contains multiple pieces + remaining = self.num_blocks - len(blocks) + if not self.layer_hashes: + # when the there is only one block for file + power2 = next_power_2(len(blocks)) + remaining = power2 - len(blocks) -

Edit the editable values of the torrent meta file.

+ # pad the the rest with zeroes to fill remaining space. + padding = [bytes(32) for _ in range(remaining)] + self.prog_update(HASH_SIZE * remaining) + blocks.extend(padding) + # calculate the root hash for the merkle tree up to piece-length -
- Source code in torrentfile\interactive.py -
def edit_action():
-    """
-    Edit the editable values of the torrent meta file.
-    """
-    showcenter("Edit Torrent")
-    metafile = get_input("Metafile(.torrent): ", os.path.exists)
-    dialog = InteractiveEditor(metafile)
-    dialog.show_current()
-    dialog.edit_props()
-
-
-
+ layer_hash = merkle_root(blocks) + if self._cb: + self._cb(layer_hash) + self.layer_hashes.append(layer_hash) + self._calculate_root() + self.prog_close() -
+ def _calculate_root(self): + """ + Calculate root hash for the target file. + **DEPRECATED** + """ + self.piece_layer = b"".join(self.layer_hashes) + hashes = len(self.layer_hashes) + if hashes > 1: + pow2 = next_power_2(hashes) + remainder = pow2 - hashes + pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)] + for _ in range(remainder): + self.layer_hashes.append(merkle_root(pad_piece)) + self.root = merkle_root(self.layer_hashes) + +
+
-
+ +
-

-get_input(*args) -

-
-

Determine appropriate input function to call.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
argstuple

Arbitrary number of args to pass to next function

()
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

The results of the function call.

-
- Source code in torrentfile\interactive.py -
def get_input(*args: tuple):  # pragma: no cover
-    """
-    Determine appropriate input function to call.
 
-    Parameters
-    ----------
-    args : tuple
-        Arbitrary number of args to pass to next function
 
-    Returns
-    -------
-    str
-        The results of the function call.
-    """
-    if len(args) == 2:
-        return _get_input_loop(*args)
-    return _get_input(*args)
-
-
-
-
@@ -5510,34 +7714,53 @@

-

-recheck_torrent() +

+__init__(path, piece_length, progress=True) -
+
+
-

Check torrent download completed percentage.

+

Calculate and store hash information for specific file.

+

DEPRECATED

- Source code in torrentfile\interactive.py -
def recheck_torrent():
+          Source code in torrentfile\hasher.py
+          
190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
def __init__(self, path: str, piece_length: int, progress: bool = True):
     """
-    Check torrent download completed percentage.
+    Calculate and store hash information for specific file.
+
+    **DEPRECATED**
     """
-    showcenter("Check Torrent")
-    msg = "Enter path to torrent contents, and corresponding torrent metafile."
-    showtext(msg)
-    metafile = get_input(
-        "Conent Path (downloads/complete/torrentname):", os.path.exists
-    )
-    contents = get_input("Metafile (*.torrent): ", os.path.exists)
-    checker = Checker(metafile, contents)
-    results = checker.results()
-    showtext(f"Completion for {metafile} is {results}%")
-    return results
+    self.path = path
+    self.root = None
+    self.piece_layer = None
+    self.layer_hashes = []
+    self.piece_length = piece_length
+    self.num_blocks = piece_length // BLOCK_SIZE
+    if progress:
+        self.prog_start(os.path.getsize(path), path, unit="bytes")
+    with open(self.path, "rb") as fd:
+        self.process_file(fd)
 
+
@@ -5549,40 +7772,51 @@

-

-select_action() +

+_calculate_root() -
+
+
-

Operate TorrentFile program interactively through terminal.

+

Calculate root hash for the target file.

+

DEPRECATED

- Source code in torrentfile\interactive.py -
def select_action():
-    """
-    Operate TorrentFile program interactively through terminal.
-    """
-    showcenter("TorrentFile: Starting Interactive Mode")
-    action = get_input(
-        "Enter the action you wish to perform.\n"
-        "Action ( Create (c) | Edit (e) | Recheck (r) ): "
-    )
-    action = action.lower()
-
-    if "create" in action or action == "c":
-        return create_torrent()
-
-    if "check" in action or action == "r":
-        return recheck_torrent()
+          Source code in torrentfile\hasher.py
+          
255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
def _calculate_root(self):
+    """
+    Calculate root hash for the target file.
 
-    if "edit" in action or action == "e":
-        return edit_action()
-    print("Unable to recognize input.  Please try again.")  # pragma: nocover
-    return select_action()  # pragma: nocover
+    **DEPRECATED**
+    """
+    self.piece_layer = b"".join(self.layer_hashes)
+    hashes = len(self.layer_hashes)
+    if hashes > 1:
+        pow2 = next_power_2(hashes)
+        remainder = pow2 - hashes
+        pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)]
+        for _ in range(remainder):
+            self.layer_hashes.append(merkle_root(pad_piece))
+    self.root = merkle_root(self.layer_hashes)
 
+
@@ -5594,55 +7828,154 @@

-

-showcenter(txt) +

+process_file(fd) -
+
+
-

Print text to screen in the center position of the terminal.

+

Calculate hashes over 16KiB chuncks of file content.

+

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
txtstr

the preformated message to send to stdout.

requiredNameTypeDescriptionDefault
+ + + + fd + + TextIOWrapper + +

Opened file in read mode.

+ + required + + + + +
- Source code in torrentfile\interactive.py -
def showcenter(txt: str):
+          Source code in torrentfile\hasher.py
+          
207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
def process_file(self, fd: str):
     """
-    Print text to screen in the center position of the terminal.
+    Calculate hashes over 16KiB chuncks of file content.
+
+    **DEPRECATED**
 
     Parameters
     ----------
-    txt : str
-        the preformated message to send to stdout.
+    fd : TextIOWrapper
+        Opened file in read mode.
     """
-    termlen = shutil.get_terminal_size().columns
-    padding = " " * int(((termlen - len(txt)) / 2))
-    string = "".join(["\n", padding, txt, "\n"])
-    showtext(string)
-
- - + while True: + blocks = [] + leaf = bytearray(BLOCK_SIZE) + # generate leaves of merkle tree + + for _ in range(self.num_blocks): + size = fd.readinto(leaf) + self.prog_update(size) + if not size: + break + blocks.append(sha256(leaf[:size]).digest()) + + # blocks is empty mean eof + if not blocks: + break + if len(blocks) != self.num_blocks: + # when size of file doesn't fill the last block + # when the file contains multiple pieces + remaining = self.num_blocks - len(blocks) + if not self.layer_hashes: + # when the there is only one block for file + power2 = next_power_2(len(blocks)) + remaining = power2 - len(blocks) + + # pad the the rest with zeroes to fill remaining space. + padding = [bytes(32) for _ in range(remaining)] + self.prog_update(HASH_SIZE * remaining) + blocks.extend(padding) + # calculate the root hash for the merkle tree up to piece-length + + layer_hash = merkle_root(blocks) + if self._cb: + self._cb(layer_hash) + self.layer_hashes.append(layer_hash) + self._calculate_root() + self.prog_close() + +
+
+
+ +
+ + + + + +
+ +
+ +
-
@@ -5650,48 +7983,104 @@

-

-showtext(txt) +

+merkle_root(blocks)

+
-

Print contents of txt to screen.

+

Calculate the merkle root for a seq of sha256 hash digests.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
txtstr

text to print to terminal.

requiredNameTypeDescriptionDefault
+ + + + blocks + + list + +

a sequence of sha256 layer hashes.

+ + required + + + + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ bytes +

the sha256 root hash of the merkle tree.

+
- Source code in torrentfile\interactive.py -
def showtext(txt):
+          Source code in torrentfile\hasher.py
+          
146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
def merkle_root(blocks: list) -> bytes:
     """
-    Print contents of txt to screen.
+    Calculate the merkle root for a seq of sha256 hash digests.
 
     Parameters
     ----------
-    txt : str
-        text to print to terminal.
+    blocks : list
+        a sequence of sha256 layer hashes.
+
+    Returns
+    -------
+    bytes
+        the sha256 root hash of the merkle tree.
     """
-    sys.stdout.write(txt)
+    if blocks:
+        while len(blocks) > 1:
+            blocks = [
+                sha256(x + y).digest() for x, y in zip(*[iter(blocks)] * 2)
+            ]
+        return blocks[0]
+    return blocks
 
+
@@ -5701,7 +8090,6 @@

-

@@ -5714,8 +8102,8 @@

-

- mixins +

+ interactive @@ -5723,9 +8111,7 @@

-

Collection of classes that can be used as Mixins with other base classes.

-

Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the -progress bar mixin. And any class is eligible to use the callback mixin.

+

Module contains the procedures used for Interactive Mode.

@@ -5741,40 +8127,204 @@

-

- -CbMixin +

+ InteractiveCreator

+
-

Mixin class to set a callback during hashing procedure.

-
- Source code in torrentfile\mixins.py -
class CbMixin:
+      

Class namespace for interactive program options.

+ + +
+ Source code in torrentfile\interactive.py +
293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
class InteractiveCreator:
     """
-    Mixin class to set a callback during hashing procedure.
+    Class namespace for interactive program options.
     """
 
-    _cb = None
-
-    @classmethod
-    def set_callback(cls, func):
+    def __init__(self):
         """
-        Assign a callback to the Hashing class.
+        Initialize interactive meta file creator dialog.
+        """
+        self.kwargs = {
+            "announce": None,
+            "url_list": None,
+            "private": None,
+            "source": None,
+            "comment": None,
+            "piece_length": None,
+            "outfile": None,
+            "path": None,
+            "httpseeds": None,
+        }
+        self.outfile, self.meta = self.get_props()
 
-        Parameters
-        ----------
-        func : function
-            the callback function
+    def get_props(self):
+        """
+        Gather details for torrentfile from user.
         """
-        cls._cb = func  # pragma: nocover
+        piece_length = get_input(
+            "Piece Length (empty=auto): ", lambda x: x.isdigit()
+        )
+
+        self.kwargs["piece_length"] = piece_length
+        announce = get_input(
+            "Tracker list (empty): ", lambda x: isinstance(x, str)
+        )
+
+        if announce:
+            self.kwargs["announce"] = announce.split()
+
+        url_list = get_input(
+            "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str)
+        )
+
+        httpseeds = get_input(
+            "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str)
+        )
+
+        if url_list:
+            self.kwargs["url_list"] = url_list.split()
+        if httpseeds:
+            self.kwargs["httpseeds"] = httpseeds.split()
+        comment = get_input("Comment (empty): ", None)
+
+        if comment:
+            self.kwargs["comment"] = comment
+        source = get_input("Source (empty): ", None)
+
+        if source:
+            self.kwargs["source"] = source
+
+        private = get_input(
+            "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN"
+        )
+
+        if private and private.lower() == "y":
+            self.kwargs["private"] = 1
+
+        contents = get_input("Content Path: ", os.path.exists)
+        self.kwargs["path"] = contents
+
+        outfile = get_input(
+            f"Output Path ({contents}.torrent): ",
+            lambda x: os.path.exists(os.path.dirname(x)),
+        )
+
+        if outfile:
+            self.kwargs["outfile"] = outfile
+
+        meta_version = get_input(
+            "Meta Version {1,2,3}: (1)", lambda x: x in "123"
+        )
+
+        showcenter(f"creating {outfile}")
+
+        if meta_version == "3":
+            torrent = TorrentFileHybrid(**self.kwargs)
+        elif meta_version == "2":
+            torrent = TorrentFileV2(**self.kwargs)
+        else:
+            torrent = TorrentFile(**self.kwargs)
+        return torrent.write()
 
- +
+
@@ -5788,56 +8338,56 @@

-
+
-
-set_callback(func) +
+__init__() - - classmethod -
+
-

Assign a callback to the Hashing class.

+

Initialize interactive meta file creator dialog.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
funcfunction

the callback function

required
- Source code in torrentfile\mixins.py -
@classmethod
-def set_callback(cls, func):
+          Source code in torrentfile\interactive.py
+          
298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
def __init__(self):
     """
-    Assign a callback to the Hashing class.
-
-    Parameters
-    ----------
-    func : function
-        the callback function
+    Initialize interactive meta file creator dialog.
     """
-    cls._cb = func  # pragma: nocover
+    self.kwargs = {
+        "announce": None,
+        "url_list": None,
+        "private": None,
+        "source": None,
+        "comment": None,
+        "piece_length": None,
+        "outfile": None,
+        "path": None,
+        "httpseeds": None,
+    }
+    self.outfile, self.meta = self.get_props()
 
+
@@ -5845,122 +8395,401 @@
+
-
-
+
+get_props() -
+

-
+
+

Gather details for torrentfile from user.

+
+ Source code in torrentfile\interactive.py +
315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
def get_props(self):
+    """
+    Gather details for torrentfile from user.
+    """
+    piece_length = get_input(
+        "Piece Length (empty=auto): ", lambda x: x.isdigit()
+    )
 
-

- -ProgMixin + self.kwargs["piece_length"] = piece_length + announce = get_input( + "Tracker list (empty): ", lambda x: isinstance(x, str) + ) + if announce: + self.kwargs["announce"] = announce.split() + url_list = get_input( + "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str) + ) -

+ httpseeds = get_input( + "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str) + ) -
+ if url_list: + self.kwargs["url_list"] = url_list.split() + if httpseeds: + self.kwargs["httpseeds"] = httpseeds.split() + comment = get_input("Comment (empty): ", None) -

Progress bar mixin class.

-

Displays progress of hashing individual files, usefull when hashing -really big files.

-
Methods
-

prog_start -prog_update -prog_close

+ if comment: + self.kwargs["comment"] = comment + source = get_input("Source (empty): ", None) -
- Source code in torrentfile\mixins.py -
class ProgMixin:
-    """
-    Progress bar mixin class.
+    if source:
+        self.kwargs["source"] = source
 
-    Displays progress of hashing individual files, usefull when hashing
-    really big files.
+    private = get_input(
+        "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN"
+    )
 
-    Methods
-    -------
-    prog_start
-    prog_update
-    prog_close
+    if private and private.lower() == "y":
+        self.kwargs["private"] = 1
+
+    contents = get_input("Content Path: ", os.path.exists)
+    self.kwargs["path"] = contents
+
+    outfile = get_input(
+        f"Output Path ({contents}.torrent): ",
+        lambda x: os.path.exists(os.path.dirname(x)),
+    )
+
+    if outfile:
+        self.kwargs["outfile"] = outfile
+
+    meta_version = get_input(
+        "Meta Version {1,2,3}: (1)", lambda x: x in "123"
+    )
+
+    showcenter(f"creating {outfile}")
+
+    if meta_version == "3":
+        torrent = TorrentFileHybrid(**self.kwargs)
+    elif meta_version == "2":
+        torrent = TorrentFileV2(**self.kwargs)
+    else:
+        torrent = TorrentFile(**self.kwargs)
+    return torrent.write()
+
+
+
+
+ +
+ + + + + +
+ +
+ +
+ + + +
+ + + +

+ InteractiveEditor + + + +

+ + +
+ + +

Interactive dialog class for torrent editing.

+ + +
+ Source code in torrentfile\interactive.py +
190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
class InteractiveEditor:
+    """
+    Interactive dialog class for torrent editing.
     """
 
-    def prog_start(self, total, path, length=50, unit=None):
+    def __init__(self, metafile: str):
         """
-        Generate a new progress bar for the given file path.
+        Initialize the Interactive torrent editor guide.
 
         Parameters
         ----------
-        total : int
-            the total amount of units accumulating towards.
-        path : str
-            path to file being hashed.
-        length : int
-            the number of characters of the actual progress bar.
-        unit : str
-            the text representation of the value being measured.
+        metafile : str
+            user input string identifying the path to a torrent meta file.
         """
-        title = path
-        width = shutil.get_terminal_size().columns
-        if len(str(title)) >= width // 2:
-            parts = list(Path(title).parts)
-            while len("//".join(parts)) > width // 2 and len(parts) > 0:
-                del parts[0]
-            title = os.path.join(*parts)
-        length = min(length, width // 2)
-        start = width - int(length * 1.5)
-        self.prog = ProgressBar(total, title, length, unit, start)
+        self.metafile = metafile
+        self.meta = pyben.load(metafile)
+        self.info = self.meta["info"]
 
-    def prog_update(self, val):
+        self.args = {
+            "url-list": self.meta.get("url-list", None),
+            "httpseeds": self.meta.get("httpseeds", None),
+            "announce": self.meta.get("announce-list", None),
+            "source": self.info.get("source", None),
+            "private": self.info.get("private", None),
+            "comment": self.info.get("comment", None),
+        }
+
+    def show_current(self):
         """
-        Update progress bar with given amount of progress.
+        Display the current met file information to screen.
+        """
+        out = "Current properties and values:\n"
+        longest = max([len(label) for label in self.args]) + 3
+        for key, val in self.args.items():
+            txt = (key.title() + ":").ljust(longest) + str(val)
+            out += f"\t{txt}\n"
+        showtext(out)
+
+    def sanatize_response(self, key, response):
+        """
+        Convert the input data into a form recognizable by the program.
 
         Parameters
         ----------
-        val : int
-            the number of bytes count the progress bar should increase.
+        key : str
+            name of the property and attribute being eddited.
+        response : str
+            User input value the property is being edited to.
         """
-        if self.is_active():
-            self.prog.increment(val)
-            pbar = self.prog.pbar()
-            output = f"{self.prog.prefix}{pbar}{self.prog.suffix}\r"
-            sys.stdout.write(output)
-            sys.stdout.flush()
+        if key in ["announce", "url-list", "httpseeds"]:
+            val = response.split()
+        else:
+            val = response
+        self.args[key] = val
 
-    def prog_close(self):
+    def edit_props(self):
         """
-        Finalize the last bits of progress bar.
-
-        Increment the terminal by one line leaving the progress bar in place,
-        and deleting the progress bar object to clear a space for the next one.
+        Loop continuosly for edits until user signals DONE.
         """
-        if self.is_active():
-            sys.stdout.flush()
-            sys.stdout.write("\n")
-            del self.prog
+        while True:
+            showcenter(
+                "Choose the number for a propert the needs editing."
+                "Enter DONE when all editing has been completed."
+            )
 
-    def is_active(self):
-        """
-        Test to see if there is an active progress bar for object.
+            props = {
+                1: "comment",
+                2: "source",
+                3: "private",
+                4: "tracker",
+                5: "web-seed",
+                6: "httpseeds",
+            }
 
-        Returns
-        -------
-        bool :
-            True if there is, otherwise False.
-        """
-        if hasattr(self, "prog"):
-            return True
-        return False
+            args = {
+                1: "comment",
+                2: "source",
+                3: "private",
+                4: "announce",
+                5: "url-list",
+                6: "httpseeds",
+            }
+
+            txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
+            prop = get_input(txt)
+            if prop.lower() == "done":
+                break
+
+            if prop.isdigit() and 0 < int(prop) < 6:
+                key = props[int(prop)]
+                key2 = args[int(prop)]
+                val = self.args.get(key2)
+                showtext(
+                    "Enter new property value or leave empty for no value."
+                )
+                response = get_input(f"{key.title()} ({val}): ")
+                self.sanatize_response(key2, response)
+
+            else:
+                showtext("Invalid input: Try again.")
+        edit_torrent(self.metafile, self.args)
 
- +
+
@@ -5974,35 +8803,93 @@
Methods
-
-
-is_active(self) +
+ + + +
+__init__(metafile)
+
-

Test to see if there is an active progress bar for object.

+

Initialize the Interactive torrent editor guide.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
metafile + str +

user input string identifying the path to a torrent meta file.

+ required +
- Source code in torrentfile\mixins.py -
def is_active(self):
+          Source code in torrentfile\interactive.py
+          
195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
def __init__(self, metafile: str):
     """
-    Test to see if there is an active progress bar for object.
+    Initialize the Interactive torrent editor guide.
 
-    Returns
-    -------
-    bool :
-        True if there is, otherwise False.
+    Parameters
+    ----------
+    metafile : str
+        user input string identifying the path to a torrent meta file.
     """
-    if hasattr(self, "prog"):
-        return True
-    return False
+    self.metafile = metafile
+    self.meta = pyben.load(metafile)
+    self.info = self.meta["info"]
+
+    self.args = {
+        "url-list": self.meta.get("url-list", None),
+        "httpseeds": self.meta.get("httpseeds", None),
+        "announce": self.meta.get("announce-list", None),
+        "source": self.info.get("source", None),
+        "private": self.info.get("private", None),
+        "comment": self.info.get("comment", None),
+    }
 
+
@@ -6010,36 +8897,116 @@
-
+
-
-prog_close(self) +
+edit_props()
+
-

Finalize the last bits of progress bar.

-

Increment the terminal by one line leaving the progress bar in place, -and deleting the progress bar object to clear a space for the next one.

+

Loop continuosly for edits until user signals DONE.

- Source code in torrentfile\mixins.py -
def prog_close(self):
+          Source code in torrentfile\interactive.py
+          
245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
def edit_props(self):
     """
-    Finalize the last bits of progress bar.
-
-    Increment the terminal by one line leaving the progress bar in place,
-    and deleting the progress bar object to clear a space for the next one.
+    Loop continuosly for edits until user signals DONE.
     """
-    if self.is_active():
-        sys.stdout.flush()
-        sys.stdout.write("\n")
-        del self.prog
+    while True:
+        showcenter(
+            "Choose the number for a propert the needs editing."
+            "Enter DONE when all editing has been completed."
+        )
+
+        props = {
+            1: "comment",
+            2: "source",
+            3: "private",
+            4: "tracker",
+            5: "web-seed",
+            6: "httpseeds",
+        }
+
+        args = {
+            1: "comment",
+            2: "source",
+            3: "private",
+            4: "announce",
+            5: "url-list",
+            6: "httpseeds",
+        }
+
+        txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
+        prop = get_input(txt)
+        if prop.lower() == "done":
+            break
+
+        if prop.isdigit() and 0 < int(prop) < 6:
+            key = props[int(prop)]
+            key2 = args[int(prop)]
+            val = self.args.get(key2)
+            showtext(
+                "Enter new property value or leave empty for no value."
+            )
+            response = get_input(f"{key.title()} ({val}): ")
+            self.sanatize_response(key2, response)
+
+        else:
+            showtext("Invalid input: Try again.")
+    edit_torrent(self.metafile, self.args)
 
+
@@ -6047,85 +9014,90 @@
-
+
-
-prog_start(self, total, path, length=50, unit=None) +
+sanatize_response(key, response)
+
-

Generate a new progress bar for the given file path.

+

Convert the input data into a form recognizable by the program.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
totalint

the total amount of units accumulating towards.

required
pathstr

path to file being hashed.

required
lengthint

the number of characters of the actual progress bar.

50
+ - - - - + + + + - -
unitstr

the text representation of the value being measured.

NoneNameTypeDescriptionDefault
+ + + + key + + str + +

name of the property and attribute being eddited.

+ + required + + + + response + + str + +

User input value the property is being edited to.

+ + required + + + + +
- Source code in torrentfile\mixins.py -
def prog_start(self, total, path, length=50, unit=None):
+          Source code in torrentfile\interactive.py
+          
228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
def sanatize_response(self, key, response):
     """
-    Generate a new progress bar for the given file path.
+    Convert the input data into a form recognizable by the program.
 
     Parameters
     ----------
-    total : int
-        the total amount of units accumulating towards.
-    path : str
-        path to file being hashed.
-    length : int
-        the number of characters of the actual progress bar.
-    unit : str
-        the text representation of the value being measured.
+    key : str
+        name of the property and attribute being eddited.
+    response : str
+        User input value the property is being edited to.
     """
-    title = path
-    width = shutil.get_terminal_size().columns
-    if len(str(title)) >= width // 2:
-        parts = list(Path(title).parts)
-        while len("//".join(parts)) > width // 2 and len(parts) > 0:
-            del parts[0]
-        title = os.path.join(*parts)
-    length = min(length, width // 2)
-    start = width - int(length * 1.5)
-    self.prog = ProgressBar(total, title, length, unit, start)
+    if key in ["announce", "url-list", "httpseeds"]:
+        val = response.split()
+    else:
+        val = response
+    self.args[key] = val
 
+
@@ -6133,57 +9105,44 @@
-
+
-
-prog_update(self, val) +
+show_current()
+
-

Update progress bar with given amount of progress.

+

Display the current met file information to screen.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
valint

the number of bytes count the progress bar should increase.

required
- Source code in torrentfile\mixins.py -
def prog_update(self, val):
+          Source code in torrentfile\interactive.py
+          
217
+218
+219
+220
+221
+222
+223
+224
+225
+226
def show_current(self):
     """
-    Update progress bar with given amount of progress.
-
-    Parameters
-    ----------
-    val : int
-        the number of bytes count the progress bar should increase.
+    Display the current met file information to screen.
     """
-    if self.is_active():
-        self.prog.increment(val)
-        pbar = self.prog.pbar()
-        output = f"{self.prog.prefix}{pbar}{self.prog.suffix}\r"
-        sys.stdout.write(output)
-        sys.stdout.flush()
+    out = "Current properties and values:\n"
+    longest = max([len(label) for label in self.args]) + 3
+    for key, val in self.args.items():
+        txt = (key.title() + ":").ljust(longest) + str(val)
+        out += f"\t{txt}\n"
+    showtext(out)
 
+
@@ -6201,198 +9160,275 @@
-
+
-

- -ProgressBar +

+_get_input(txt)

+
-

Holds the state and details of the terminal progress bars.

+

Gather information needed from user.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
totalint

the total amount to be accumulated.

required
titlestr

the subject of the progress tracker

required
+ - - - - + + + + + + + + + + + + + +
lengthint

the width of the progress bar

requiredNameTypeDescriptionDefault
txt + str +

The message usually containing instructions for the user.

+ required +
+ +

Returns:

+ + - - - - + + - -
unitstr

the text representation incremented

requiredTypeDescription
+ + + + + str + +

The text input received from the user.

+ + + +
- Source code in torrentfile\mixins.py -
class ProgressBar:
-    """
-    Holds the state and details of the terminal progress bars.
+          Source code in torrentfile\interactive.py
+          
53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
def _get_input(txt: str):  # pragma: no cover
+    """
+    Gather information needed from user.
 
     Parameters
     ----------
-    total : int
-        the total amount to be accumulated.
-    title : str
-        the subject of the progress tracker
-    length : int
-        the width of the progress bar
-    unit : str
-        the text representation incremented
+    txt : str
+        The message usually containing instructions for the user.
+
+    Returns
+    -------
+    str
+        The text input received from the user.
     """
+    value = input(txt)
+    return value
+
+
+
+
- def __init__(self, total, title, length, unit, start): - """ - Construct the progress bar object and store state of it's properties. - """ - self.total = total - self.start = start - self.length = length - self.fill = chr(9608) - self.empty = chr(9617) - self.state = 0 - self.unit = unit - self.show_total = total - if not unit: - self.unit = "" # pragma: nocover - elif unit == "bytes": - if self.total > 10000000: - self.show_total = math.floor(self.total / 1048576) - self.unit = "MiB" - elif self.total > 10000: - self.show_total = math.floor(self.total / 1024) - self.unit = "KiB" - self.suffix = f"/{self.show_total} {self.unit}" - if len(title) > start: - title = title[: start - 1] - padding = (start - len(title)) * " " - self.prefix = "".join([title, padding]) +
- def increment(self, value): - """ - Increase the state of the progress bar value. - Parameters - ---------- - value : int - the amount to increment the state by. - """ - self.state += value - def pbar(self): - """ - Return the size of the filled portion of the progress bar. +
- Returns - ------- - str : - the progress bar characters - """ - if self.state >= self.total: - fill = self.length - else: - fill = math.ceil((self.state / self.total) * self.length) - empty = self.length - fill - if self.unit == "MiB": - state = math.floor(self.state / 1048576) - elif self.unit == "KiB": - state = math.floor(self.state / 1024) - else: - state = self.state - progbar = ["[", self.fill * fill, self.empty * empty, "] ", str(state)] - return "".join(progbar) -
- +

+_get_input_loop(txt, func) + + +

-
+
+ +

Gather information needed from user.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
txt + str +

The message usually containing instructions for the user.

+ required +
func + function +

Validate/Check user input data, failure = retry, success = continue.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

The text input received from the user.

+
+ Source code in torrentfile\interactive.py +
71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
def _get_input_loop(txt, func):  # pragma: no cover
+    """
+    Gather information needed from user.
 
+    Parameters
+    ----------
+    txt : str
+        The message usually containing instructions for the user.
+    func : function
+        Validate/Check user input data, failure = retry, success = continue.
 
+    Returns
+    -------
+    str
+        The text input received from the user.
+    """
+    while True:
+        value = input(txt)
+        if func and func(value):
+            return value
+        if not func or value == "":
+            return value
+        showtext(f"Invalid input {value}: try again")
+
+
+
+
+
+
-
+

+create_torrent() -

-__init__(self, total, title, length, unit, start) - - special - +
-
-

Construct the progress bar object and store state of it's properties.

+

Create new torrent file interactively.

- Source code in torrentfile\mixins.py -
def __init__(self, total, title, length, unit, start):
+          Source code in torrentfile\interactive.py
+          
163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
def create_torrent():
     """
-    Construct the progress bar object and store state of it's properties.
+    Create new torrent file interactively.
     """
-    self.total = total
-    self.start = start
-    self.length = length
-    self.fill = chr(9608)
-    self.empty = chr(9617)
-    self.state = 0
-    self.unit = unit
-    self.show_total = total
-    if not unit:
-        self.unit = ""  # pragma: nocover
-    elif unit == "bytes":
-        if self.total > 10000000:
-            self.show_total = math.floor(self.total / 1048576)
-            self.unit = "MiB"
-        elif self.total > 10000:
-            self.show_total = math.floor(self.total / 1024)
-            self.unit = "KiB"
-    self.suffix = f"/{self.show_total} {self.unit}"
-    if len(title) > start:
-        title = title[: start - 1]
-    padding = (start - len(title)) * " "
-    self.prefix = "".join([title, padding])
+    showcenter("Create Torrent")
+    showtext(
+        "\nEnter values for each of the options for the torrent creator, "
+        "or leave blank for program defaults.\nSpaces are considered item "
+        "seperators for options that accept a list of values.\nValues "
+        "enclosed in () indicate the default value, while {} holds all "
+        "valid choices available for the option.\n\n"
+    )
+    creator = InteractiveCreator()
+    return creator
 
+
@@ -6400,52 +9436,42 @@
-
+
-
-increment(self, value) +

+edit_action() -

+
+
-

Increase the state of the progress bar value.

+

Edit the editable values of the torrent meta file.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
valueint

the amount to increment the state by.

required
- Source code in torrentfile\mixins.py -
def increment(self, value):
+          Source code in torrentfile\interactive.py
+          
179
+180
+181
+182
+183
+184
+185
+186
+187
def edit_action():
     """
-    Increase the state of the progress bar value.
-
-    Parameters
-    ----------
-    value : int
-        the amount to increment the state by.
+    Edit the editable values of the torrent meta file.
     """
-    self.state += value
+    showcenter("Edit Torrent")
+    metafile = get_input("Metafile(.torrent): ", os.path.exists)
+    dialog = InteractiveEditor(metafile)
+    dialog.show_current()
+    dialog.edit_props()
 
+
@@ -6453,45 +9479,100 @@
-
+
-
-pbar(self) +

+get_input(*args) -

+
+
-

Return the size of the filled portion of the progress bar.

+

Determine appropriate input function to call.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + tuple +

Arbitrary number of args to pass to next function

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

The results of the function call.

- Source code in torrentfile\mixins.py -
def pbar(self):
+          Source code in torrentfile\interactive.py
+          
34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
def get_input(*args: tuple):  # pragma: no cover
     """
-    Return the size of the filled portion of the progress bar.
+    Determine appropriate input function to call.
+
+    Parameters
+    ----------
+    args : tuple
+        Arbitrary number of args to pass to next function
 
     Returns
     -------
-    str :
-        the progress bar characters
+    str
+        The results of the function call.
     """
-    if self.state >= self.total:
-        fill = self.length
-    else:
-        fill = math.ceil((self.state / self.total) * self.length)
-    empty = self.length - fill
-    if self.unit == "MiB":
-        state = math.floor(self.state / 1048576)
-    elif self.unit == "KiB":
-        state = math.floor(self.state / 1024)
-    else:
-        state = self.state
-    progbar = ["[", self.fill * fill, self.empty * empty, "] ", str(state)]
-    return "".join(progbar)
+    if len(args) == 2:
+        return _get_input_loop(*args)
+    return _get_input(*args)
 
+
@@ -6499,105 +9580,54 @@
- - -
- -
- -
- - - -
-

-waiting(msg, flag, timeout=180) +

+recheck_torrent()

+
-

Show loading message while thread completes processing.

+

Check torrent download completed percentage.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
msgstr

Message string printed before the progress bar

required
flaglist

Once flag is filled exit loop

required
timeoutint

max amount of time to run the function.

180
- Source code in torrentfile\mixins.py -
def waiting(msg, flag, timeout=180):
+          Source code in torrentfile\interactive.py
+          
146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
def recheck_torrent():
     """
-    Show loading message while thread completes processing.
-
-    Parameters
-    ----------
-    msg : str
-        Message string printed before the progress bar
-    flag : list
-        Once flag is filled exit loop
-    timeout : int
-        max amount of time to run the function.
+    Check torrent download completed percentage.
     """
-    then = time.time()
-    codes, fill = list(range(9617, 9620)), chr(9619)
-    size = idx = 0
-    total = shutil.get_terminal_size().columns - len(msg) - 20
-
-    def output(text):
-        """
-        Print parameter message to the console.
-
-        Parameters
-        ----------
-        text : str
-            output message
-        """
-        sys.stdout.write(text)
-        sys.stdout.flush()
-
-    output("\n")
-    while not flag:
-        time.sleep(0.16)
-        filled = (fill * size) + chr(codes[idx]) + (" " * (total - size))
-        output(f"{msg}: {filled}\r")
-        idx = idx + 1 if idx + 1 < len(codes) else 0
-        size = size + 1 if size < total else 0
-        if time.time() - then > timeout:
-            break
-    output("\n")
+    showcenter("Check Torrent")
+    msg = "Enter path to torrent contents, and corresponding torrent metafile."
+    showtext(msg)
+    metafile = get_input(
+        "Conent Path (downloads/complete/torrentname):", os.path.exists
+    )
+    contents = get_input("Metafile (*.torrent): ", os.path.exists)
+    checker = Checker(metafile, contents)
+    results = checker.results()
+    showtext(f"Completion for {metafile} is {results}%")
+    return results
 
+
@@ -6605,480 +9635,392 @@

+
-
+

+select_action() -

-
+
+
-
+

Operate TorrentFile program interactively through terminal.

+
+ Source code in torrentfile\interactive.py +
123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
def select_action():
+    """
+    Operate TorrentFile program interactively through terminal.
+    """
+    showcenter("TorrentFile: Starting Interactive Mode")
+    action = get_input(
+        "Enter the action you wish to perform.\n"
+        "Action ( Create (c) | Edit (e) | Recheck (r) ): "
+    )
+    action = action.lower()
 
+    if "create" in action or action == "c":
+        return create_torrent()
 
-

- recheck + if "check" in action or action == "r": + return recheck_torrent() + if "edit" in action or action == "e": + return edit_action() + print("Unable to recognize input. Please try again.") # pragma: nocover + return select_action() # pragma: nocover +

+
+
+
+
-
-
-

Module container Checker Class.

-

The CheckerClass takes a torrentfile and tha path to it's contents. -It will then iterate through every file and directory contained -and compare their data to values contained within the torrent file. -Completion percentages will be printed to screen for each file and -at the end for the torrentfile as a whole.

+
-
+

+showcenter(txt) +

+
+

Print text to screen in the center position of the terminal.

+

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
txt + str +

the preformated message to send to stdout.

+ required +
+
+ Source code in torrentfile\interactive.py +
108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
def showcenter(txt: str):
+    """
+    Print text to screen in the center position of the terminal.
 
+    Parameters
+    ----------
+    txt : str
+        the preformated message to send to stdout.
+    """
+    termlen = shutil.get_terminal_size().columns
+    padding = " " * int(((termlen - len(txt)) / 2))
+    string = "".join(["\n", padding, txt, "\n"])
+    showtext(string)
+
+
+
+
+
-
+
-

- -Checker (ProgMixin) - +

+showtext(txt)

+
-

Check a given file or directory to see if it matches a torrentfile.

-

Public constructor for Checker class instance.

+

Print contents of txt to screen.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
metafilestr

Path to ".torrent" file.

required
+ - - - - + + + + - -
pathstr

Path where the content is located in filesystem.

requiredNameTypeDescriptionDefault
-

Examples:

-
-
-

metafile = "/path/to/torrentfile/content_file_or_dir.torrent" - >> location = "/path/to/location" - >> os.path.exists("/path/to/location/content_file_or_dir") - Out: True - >> checker = Checker(metafile, location)

-
-
+ + + + txt + + str + +

text to print to terminal.

+ + required + + + +
- Source code in torrentfile\recheck.py -
class Checker(ProgMixin):
+          Source code in torrentfile\interactive.py
+          
 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
def showtext(txt):
     """
-    Check a given file or directory to see if it matches a torrentfile.
-
-    Public constructor for Checker class instance.
+    Print contents of txt to screen.
 
     Parameters
     ----------
-    metafile : str
-        Path to ".torrent" file.
-    path : str
-        Path where the content is located in filesystem.
-
-    Example
-    -------
-        >> metafile = "/path/to/torrentfile/content_file_or_dir.torrent"
-        >> location = "/path/to/location"
-        >> os.path.exists("/path/to/location/content_file_or_dir")
-        Out: True
-        >> checker = Checker(metafile, location)
+    txt : str
+        text to print to terminal.
     """
+    sys.stdout.write(txt)
+
+
+
+
- _hook = None - - def __init__(self, metafile: str, path: str): - """ - Validate data against hashes contained in .torrent file. - - Parameters - ---------- - metafile : str - path to .torrent file - path : str - path to content or contents parent directory. - """ - if not os.path.exists(metafile): - raise FileNotFoundError - meta = [] - thread = Thread(target=pyben.loadinto, args=(metafile, meta)) - thread.start() - self.last_log = None - self.log_msg("Checking: %s, %s", metafile, path) - self.metafile = metafile - self.total = 0 - self.paths = [] - self.fileinfo = {} - thread2 = Thread(target=waiting, args=("Extracting metadata", meta)) - if not meta: # pragma: nocover - thread2.start() - thread2.join() - self.meta = meta[0] - self.info = self.meta["info"] - self.name = self.info["name"] - self.piece_length = self.info["piece length"] +
- if "meta version" in self.info: - if "pieces" in self.info: - self.meta_version = 3 - else: - self.meta_version = 2 - else: - self.meta_version = 1 - self.root = self.find_root(path) - self.check_paths() - @classmethod - def register_callback(cls, hook): - """ - Register hooks from 3rd party programs to access generated info. - Parameters - ---------- - hook : function - callback function for the logging feature. - """ - cls._hook = hook - def piece_checker(self): - """ - Check individual pieces of the torrent. +
- Returns - ------- - HashChecker | FeedChecker - Individual piece hasher. - """ - if self.meta_version == 1: - return FeedChecker - return HashChecker +
- def results(self): - """ - Generate result percentage and store for future calls. - """ - responses = [] - for response in self.iter_hashes(): - responses.append(response) +
- self.log_msg( - "Final result for %s recheck: %s", self.metafile, self._result - ) - return self._result - def log_msg(self, *args, level=logging.INFO): - """ - Log message `msg` to logger and send `msg` to callback hook. - Parameters - ---------- - args : dict - formatting args for log message - level : int - Log level for this message; default=`logging.INFO` - """ - message = args[0] - if len(args) >= 3: - message = message % tuple(args[1:]) - elif len(args) == 2: - message = message % args[1] +
- # Repeat log messages should be ignored. - if message != self.last_log: - self.last_log = message - logger.log(level, message) - if self._hook and level == logging.INFO: - self._hook(message) - def find_root(self, path: str) -> str: - """ - Check path for torrent content. - The path can be a relative or absolute filesystem path. In the case - where the content is a single file, the path may point directly to the - the file, or it may point to the parent directory. If content points - to a directory. The directory will be checked to see if it matches - the torrent's name, if not the directories contents will be searched. - The returned value will be the absolute path that matches the torrent's - name. +

+ mixins - Parameters - ---------- - path : str - root path to torrent content - Returns - ------- - str - root path to content - """ - if not os.path.exists(path): - self.log_msg("Could not locate torrent content %s.", path) - raise FileNotFoundError(path) - root = Path(path) - if root.name == self.name: - self.log_msg("Content found: %s.", str(root)) - return root +

- if self.name in os.listdir(root): - return root / self.name +
- self.log_msg("Could not locate torrent content in: %s", str(root)) - raise FileNotFoundError(root) +

Collection of classes that can be used as Mixins with other base classes.

+

Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the +progress bar mixin. And any class is eligible to use the callback mixin.

- def check_paths(self): - """ - Gather all file paths described in the torrent file. - """ - finfo = self.fileinfo - if "length" in self.info: - self.log_msg("%s points to a single file", self.root) - self.total = self.info["length"] - self.paths.append(str(self.root)) - finfo[0] = { - "path": self.root, - "length": self.info["length"], - } +
- if self.meta_version > 1: - root = self.info["file tree"][self.name][""]["pieces root"] - finfo[0]["pieces root"] = root - return - # Otherwise Content is more than 1 file. - self.log_msg("%s points to a directory", self.root) - if self.meta_version == 1: - for i, item in enumerate(self.info["files"]): - self.total += item["length"] - base = os.path.join(*item["path"]) - self.fileinfo[i] = { - "path": str(self.root / base), - "length": item["length"], - } - self.paths.append(str(self.root / base)) - return - self.walk_file_tree(self.info["file tree"], []) +
- def walk_file_tree(self, tree: dict, partials: list): - """ - Traverse File Tree dictionary to get file details. - Extract full pathnames, length, root hash, and layer hashes - for each file included in the .torrent's file tree. - Parameters - ---------- - tree : dict - File Tree dict extracted from torrent file. - partials : list - list of intermediate pathnames. - """ - for key, val in tree.items(): +

+ CbMixin - # Empty string means the tree's leaf is value - if "" in val: - base = os.path.join(*partials, key) - roothash = None - length = val[""]["length"] - roothash = None if not length else val[""]["pieces root"] - full = str(self.root / base) - self.fileinfo[len(self.paths)] = { - "path": full, - "length": length, - "pieces root": roothash, - } - self.paths.append(full) - self.total += length - else: - self.walk_file_tree(val, partials + [key]) - def iter_hashes(self) -> tuple: - """ - Produce results of comparing torrent contents piece by piece. +

- Yields - ------ - chunck : bytes - hash of data found on disk - piece : bytes - hash of data when complete and correct - path : str - path to file being hashed - size : int - length of bytes hashed for piece - """ - matched = consumed = 0 - checker = self.piece_checker() - for chunk, piece, path, size in checker(self): - consumed += size - matching = 0 - if chunk == piece: - matching += size - matched += size - yield chunk, piece, path, size - total_consumed = str(int(consumed / self.total * 100)) - percent_matched = str(int(matched / consumed * 100)) - self.log_msg( - "Processed: %s%%, Matched: %s%%", - total_consumed, - percent_matched, - level=logging.DEBUG, - ) - self._result = (matched / consumed) * 100 if consumed > 0 else 0 -
- +
-
+

Mixin class to set a callback during hashing procedure.

+
+ Source code in torrentfile\mixins.py +
33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
class CbMixin:
+    """
+    Mixin class to set a callback during hashing procedure.
+    """
+
+    _cb = None
+
+    @classmethod
+    def set_callback(cls, func):
+        """
+        Assign a callback to the Hashing class.
+
+        Parameters
+        ----------
+        func : function
+            the callback function
+        """
+        cls._cb = func  # pragma: nocover
+
+
+
+
-
-
-__init__(self, metafile, path) + + +
+ + + +
+set_callback(func) - special + classmethod
+
-

Validate data against hashes contained in .torrent file.

+

Assign a callback to the Hashing class.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
metafilestr

path to .torrent file

required
+ - - - - + + + + - -
pathstr

path to content or contents parent directory.

requiredNameTypeDescriptionDefault
+ + + + func + + function + +

the callback function

+ + required + + + + +
- Source code in torrentfile\recheck.py -
def __init__(self, metafile: str, path: str):
+          Source code in torrentfile\mixins.py
+          
40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
@classmethod
+def set_callback(cls, func):
     """
-    Validate data against hashes contained in .torrent file.
+    Assign a callback to the Hashing class.
 
     Parameters
     ----------
-    metafile : str
-        path to .torrent file
-    path : str
-        path to content or contents parent directory.
+    func : function
+        the callback function
     """
-    if not os.path.exists(metafile):
-        raise FileNotFoundError
-    meta = []
-    thread = Thread(target=pyben.loadinto, args=(metafile, meta))
-    thread.start()
-    self.last_log = None
-    self.log_msg("Checking: %s, %s", metafile, path)
-    self.metafile = metafile
-    self.total = 0
-    self.paths = []
-    self.fileinfo = {}
-    thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
-    if not meta:  # pragma: nocover
-        thread2.start()
-        thread2.join()
-    self.meta = meta[0]
-    self.info = self.meta["info"]
-    self.name = self.info["name"]
-    self.piece_length = self.info["piece length"]
-
-    if "meta version" in self.info:
-        if "pieces" in self.info:
-            self.meta_version = 3
-        else:
-            self.meta_version = 2
-    else:
-        self.meta_version = 1
-
-    self.root = self.find_root(path)
-    self.check_paths()
+    cls._cb = func  # pragma: nocover
 
+
@@ -7086,307 +10028,275 @@
-
+
-
-check_paths(self) +
+
-
-
-

Gather all file paths described in the torrent file.

+
-
- Source code in torrentfile\recheck.py -
def check_paths(self):
-    """
-    Gather all file paths described in the torrent file.
-    """
-    finfo = self.fileinfo
 
-    if "length" in self.info:
-        self.log_msg("%s points to a single file", self.root)
-        self.total = self.info["length"]
-        self.paths.append(str(self.root))
 
-        finfo[0] = {
-            "path": self.root,
-            "length": self.info["length"],
-        }
+

+ ProgMixin - if self.meta_version > 1: - root = self.info["file tree"][self.name][""]["pieces root"] - finfo[0]["pieces root"] = root - return - # Otherwise Content is more than 1 file. - self.log_msg("%s points to a directory", self.root) - if self.meta_version == 1: +

- for i, item in enumerate(self.info["files"]): - self.total += item["length"] - base = os.path.join(*item["path"]) - self.fileinfo[i] = { - "path": str(self.root / base), - "length": item["length"], - } +
- self.paths.append(str(self.root / base)) - return - self.walk_file_tree(self.info["file tree"], []) -
-
-
+

Progress bar mixin class.

+

Displays progress of hashing individual files, usefull when hashing +really big files.

+
Methods
+

prog_start +prog_update +prog_close

-
+
+ Source code in torrentfile\mixins.py +
131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
class ProgMixin:
+    """
+    Progress bar mixin class.
 
+    Displays progress of hashing individual files, usefull when hashing
+    really big files.
 
-  
+ Methods + ------- + prog_start + prog_update + prog_close + """ + def prog_start(self, total, path, length=50, unit=None): + """ + Generate a new progress bar for the given file path. + Parameters + ---------- + total : int + the total amount of units accumulating towards. + path : str + path to file being hashed. + length : int + the number of characters of the actual progress bar. + unit : str + the text representation of the value being measured. + """ + title = path + width = shutil.get_terminal_size().columns + if len(str(title)) >= width // 2: + parts = list(Path(title).parts) + while len("//".join(parts)) > width // 2 and len(parts) > 0: + del parts[0] + title = os.path.join(*parts) + length = min(length, width // 2) + start = width - int(length * 1.5) + self.prog = ProgressBar(total, title, length, unit, start) -
-find_root(self, path) + def prog_update(self, val): + """ + Update progress bar with given amount of progress. + Parameters + ---------- + val : int + the number of bytes count the progress bar should increase. + """ + if self.is_active(): + self.prog.increment(val) + pbar = self.prog.pbar() + output = f"{self.prog.prefix}{pbar}{self.prog.suffix}\r" + sys.stdout.write(output) + sys.stdout.flush() -
+ def prog_close(self): + """ + Finalize the last bits of progress bar. -
+ Increment the terminal by one line leaving the progress bar in place, + and deleting the progress bar object to clear a space for the next one. + """ + if self.is_active(): + sys.stdout.flush() + sys.stdout.write("\n") + del self.prog -

Check path for torrent content.

-

The path can be a relative or absolute filesystem path. In the case -where the content is a single file, the path may point directly to the -the file, or it may point to the parent directory. If content points -to a directory. The directory will be checked to see if it matches -the torrent's name, if not the directories contents will be searched. -The returned value will be the absolute path that matches the torrent's -name.

+ def is_active(self): + """ + Test to see if there is an active progress bar for object. -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
pathstr

root path to torrent content

required
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

root path to content

-
- Source code in torrentfile\recheck.py -
def find_root(self, path: str) -> str:
-    """
-    Check path for torrent content.
+        Returns
+        -------
+        bool :
+            True if there is, otherwise False.
+        """
+        if hasattr(self, "prog"):
+            return True
+        return False
+
+
+
- The path can be a relative or absolute filesystem path. In the case - where the content is a single file, the path may point directly to the - the file, or it may point to the parent directory. If content points - to a directory. The directory will be checked to see if it matches - the torrent's name, if not the directories contents will be searched. - The returned value will be the absolute path that matches the torrent's - name. - Parameters - ---------- - path : str - root path to torrent content - Returns - ------- - str - root path to content - """ - if not os.path.exists(path): - self.log_msg("Could not locate torrent content %s.", path) - raise FileNotFoundError(path) +
- root = Path(path) - if root.name == self.name: - self.log_msg("Content found: %s.", str(root)) - return root - if self.name in os.listdir(root): - return root / self.name - self.log_msg("Could not locate torrent content in: %s", str(root)) - raise FileNotFoundError(root) -
- -
-
-
+
-
-iter_hashes(self) + +
+is_active()
+
-

Produce results of comparing torrent contents piece by piece.

+

Test to see if there is an active progress bar for object.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
tuple

hash of data found on disk

-
- Source code in torrentfile\recheck.py -
def iter_hashes(self) -> tuple:
-    """
-    Produce results of comparing torrent contents piece by piece.
-
-    Yields
-    ------
-    chunck : bytes
-        hash of data found on disk
-    piece : bytes
-        hash of data when complete and correct
-    path : str
-        path to file being hashed
-    size : int
-        length of bytes hashed for piece
-    """
-    matched = consumed = 0
-    checker = self.piece_checker()
-    for chunk, piece, path, size in checker(self):
-        consumed += size
-        matching = 0
-        if chunk == piece:
-            matching += size
-            matched += size
-        yield chunk, piece, path, size
-        total_consumed = str(int(consumed / self.total * 100))
-        percent_matched = str(int(matched / consumed * 100))
-        self.log_msg(
-            "Processed: %s%%, Matched: %s%%",
-            total_consumed,
-            percent_matched,
-            level=logging.DEBUG,
-        )
-    self._result = (matched / consumed) * 100 if consumed > 0 else 0
-
-
-
- -
- - - -
- - - -
-log_msg(self, *args, *, level=20) - - -
- -
- -

Log message msg to logger and send msg to callback hook.

- -

Parameters:

- - - - - - - - - - - - - - - - +

Returns:

+
NameTypeDescriptionDefault
argsdict

formatting args for log message

()
+ - - - - + + - -
levelint

Log level for this message; default=logging.INFO

20Name TypeDescription
+ + + +bool + +

True if there is, otherwise False.

+ + + +
- Source code in torrentfile\recheck.py -
def log_msg(self, *args, level=logging.INFO):
+          Source code in torrentfile\mixins.py
+          
199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
def is_active(self):
     """
-    Log message `msg` to logger and send `msg` to callback hook.
+    Test to see if there is an active progress bar for object.
 
-    Parameters
-    ----------
-    args : dict
-        formatting args for log message
-    level : int
-        Log level for this message; default=`logging.INFO`
+    Returns
+    -------
+    bool :
+        True if there is, otherwise False.
     """
-    message = args[0]
-    if len(args) >= 3:
-        message = message % tuple(args[1:])
-    elif len(args) == 2:
-        message = message % args[1]
-
-    # Repeat log messages should be ignored.
-    if message != self.last_log:
-        self.last_log = message
-        logger.log(level, message)
-        if self._hook and level == logging.INFO:
-            self._hook(message)
+    if hasattr(self, "prog"):
+        return True
+    return False
 
+
@@ -7394,50 +10304,48 @@
-
+
-
-piece_checker(self) +
+prog_close()
+
-

Check individual pieces of the torrent.

+

Finalize the last bits of progress bar.

+

Increment the terminal by one line leaving the progress bar in place, +and deleting the progress bar object to clear a space for the next one.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
HashChecker | FeedChecker

Individual piece hasher.

- Source code in torrentfile\recheck.py -
def piece_checker(self):
+          Source code in torrentfile\mixins.py
+          
187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
def prog_close(self):
     """
-    Check individual pieces of the torrent.
+    Finalize the last bits of progress bar.
 
-    Returns
-    -------
-    HashChecker | FeedChecker
-        Individual piece hasher.
+    Increment the terminal by one line leaving the progress bar in place,
+    and deleting the progress bar object to clear a space for the next one.
     """
-    if self.meta_version == 1:
-        return FeedChecker
-    return HashChecker
+    if self.is_active():
+        sys.stdout.flush()
+        sys.stdout.write("\n")
+        del self.prog
 
+
@@ -7445,56 +10353,128 @@
-
+
-
-register_callback(hook) +
+prog_start(total, path, length=50, unit=None) - - classmethod -
+
-

Register hooks from 3rd party programs to access generated info.

+

Generate a new progress bar for the given file path.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
hookfunction

callback function for the logging feature.

requiredNameTypeDescriptionDefault
+ + + + total + + int + +

the total amount of units accumulating towards.

+ + required + + + + path + + str + +

path to file being hashed.

+ + required + + + + length + + int + +

the number of characters of the actual progress bar.

+ + 50 + + + + unit + + str + +

the text representation of the value being measured.

+ + None + + + + +
- Source code in torrentfile\recheck.py -
@classmethod
-def register_callback(cls, hook):
+          Source code in torrentfile\mixins.py
+          
145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
def prog_start(self, total, path, length=50, unit=None):
     """
-    Register hooks from 3rd party programs to access generated info.
+    Generate a new progress bar for the given file path.
 
     Parameters
     ----------
-    hook : function
-        callback function for the logging feature.
+    total : int
+        the total amount of units accumulating towards.
+    path : str
+        path to file being hashed.
+    length : int
+        the number of characters of the actual progress bar.
+    unit : str
+        the text representation of the value being measured.
     """
-    cls._hook = hook
+    title = path
+    width = shutil.get_terminal_size().columns
+    if len(str(title)) >= width // 2:
+        parts = list(Path(title).parts)
+        while len("//".join(parts)) > width // 2 and len(parts) > 0:
+            del parts[0]
+        title = os.path.join(*parts)
+    length = min(length, width // 2)
+    start = width - int(length * 1.5)
+    self.prog = ProgressBar(total, title, length, unit, start)
 
+
@@ -7502,119 +10482,78 @@
-
+
-
-results(self) +
+prog_update(val)
-
- -

Generate result percentage and store for future calls.

- -
- Source code in torrentfile\recheck.py -
def results(self):
-    """
-    Generate result percentage and store for future calls.
-    """
-    responses = []
-    for response in self.iter_hashes():
-        responses.append(response)
-
-    self.log_msg(
-        "Final result for %s recheck:  %s", self.metafile, self._result
-    )
-    return self._result
-
-
-
- -
- - - -
- - - -
-walk_file_tree(self, tree, partials) - - -
-

Traverse File Tree dictionary to get file details.

-

Extract full pathnames, length, root hash, and layer hashes -for each file included in the .torrent's file tree.

+

Update progress bar with given amount of progress.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
treedict

File Tree dict extracted from torrent file.

required
+ - - - - + + + + - -
partialslist

list of intermediate pathnames.

requiredNameTypeDescriptionDefault
+ + + + val + + int + +

the number of bytes count the progress bar should increase.

+ + required + + + + +
- Source code in torrentfile\recheck.py -
def walk_file_tree(self, tree: dict, partials: list):
+          Source code in torrentfile\mixins.py
+          
171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
def prog_update(self, val):
     """
-    Traverse File Tree dictionary to get file details.
-
-    Extract full pathnames, length, root hash, and layer hashes
-    for each file included in the .torrent's file tree.
+    Update progress bar with given amount of progress.
 
     Parameters
     ----------
-    tree : dict
-        File Tree dict extracted from torrent file.
-    partials : list
-        list of intermediate pathnames.
+    val : int
+        the number of bytes count the progress bar should increase.
     """
-    for key, val in tree.items():
-
-        # Empty string means the tree's leaf is value
-        if "" in val:
-
-            base = os.path.join(*partials, key)
-            roothash = None
-            length = val[""]["length"]
-            roothash = None if not length else val[""]["pieces root"]
-            full = str(self.root / base)
-            self.fileinfo[len(self.paths)] = {
-                "path": full,
-                "length": length,
-                "pieces root": roothash,
-            }
-            self.paths.append(full)
-            self.total += length
-        else:
-            self.walk_file_tree(val, partials + [key])
+    if self.is_active():
+        self.prog.increment(val)
+        pbar = self.prog.pbar()
+        output = f"{self.prog.prefix}{pbar}{self.prog.suffix}\r"
+        sys.stdout.write(output)
+        sys.stdout.flush()
 
+
@@ -7636,209 +10575,241 @@
-

- -FeedChecker (ProgMixin) - +

+ ProgressBar

+
-

Validates torrent content.

-

Seemlesly validate torrent file contents by comparing hashes in -metafile against data on disk.

-

Parameters:

- - - - - - - - - - - - - - - - +

Holds the state and details of the terminal progress bars.

+ +

Parameters:

+
NameTypeDescriptionDefault
checkerChecker

the checker class instance.

required
+ - - - - + + + + - -
hasherAny

hashing class for calculating piece hashes. default=None

requiredNameTypeDescriptionDefault
-
- Source code in torrentfile\recheck.py -
class FeedChecker(ProgMixin):
+    
+    
+        
+          total
+          
+                int
+          
+          

the total amount to be accumulated.

+ + required + + + + title + + str + +

the subject of the progress tracker

+ + required + + + + length + + int + +

the width of the progress bar

+ + required + + + + unit + + str + +

the text representation incremented

+ + required + + + + + + +
+ Source code in torrentfile\mixins.py +
 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
class ProgressBar:
     """
-    Validates torrent content.
-
-    Seemlesly validate torrent file contents by comparing hashes in
-    metafile against data on disk.
+    Holds the state and details of the terminal progress bars.
 
     Parameters
     ----------
-    checker : object
-        the checker class instance.
-    hasher : Any
-        hashing class for calculating piece hashes. default=None
+    total : int
+        the total amount to be accumulated.
+    title : str
+        the subject of the progress tracker
+    length : int
+        the width of the progress bar
+    unit : str
+        the text representation incremented
     """
 
-    def __init__(self, checker: Checker):
+    def __init__(self, total, title, length, unit, start):
         """
-        Generate hashes of piece length data from filelist contents.
+        Construct the progress bar object and store state of it's properties.
         """
-        self.piece_length = checker.piece_length
-        self.paths = checker.paths
-        self.pieces = checker.info["pieces"]
-        self.fileinfo = checker.fileinfo
-        self.piece_map = {}
-        self.index = 0
-        self.piece_count = 0
-        self.it = None
+        self.total = total
+        self.start = start
+        self.length = length
+        self.fill = chr(9608)
+        self.empty = chr(9617)
+        self.state = 0
+        self.unit = unit
+        self.show_total = total
+        if not unit:
+            self.unit = ""  # pragma: nocover
+        elif unit == "bytes":
+            if self.total > 10000000:
+                self.show_total = math.floor(self.total / 1048576)
+                self.unit = "MiB"
+            elif self.total > 10000:
+                self.show_total = math.floor(self.total / 1024)
+                self.unit = "KiB"
+        self.suffix = f"/{self.show_total} {self.unit}"
+        if len(title) > start:
+            title = title[: start - 1]
+        padding = (start - len(title)) * " "
+        self.prefix = "".join([title, padding])
 
-    def __iter__(self):
+    def increment(self, value):
         """
-        Assign iterator and return self.
-        """
-        self.it = self.iter_pieces()
-        return self
+        Increase the state of the progress bar value.
 
-    def __next__(self):
-        """
-        Yield back result of comparison.
+        Parameters
+        ----------
+        value : int
+            the amount to increment the state by.
         """
-        try:
-            partial = next(self.it)
-        except StopIteration as itererror:
-            raise StopIteration from itererror
-
-        chunck = sha1(partial).digest()  # nosec
-        start = self.piece_count * SHA1
-        end = start + SHA1
-        piece = self.pieces[start:end]
-        self.piece_count += 1
-        path = self.paths[self.index]
-        return chunck, piece, path, len(partial)
+        self.state += value
 
-    def iter_pieces(self):
+    def pbar(self):
         """
-        Iterate through, and hash pieces of torrent contents.
+        Return the size of the filled portion of the progress bar.
 
-        Yields
-        ------
-        piece : bytes
-            hash digest for block of torrent data.
+        Returns
+        -------
+        str :
+            the progress bar characters
         """
-        partial = bytearray()
-        for i, path in enumerate(self.paths):
-            total = self.fileinfo[i]["length"]
-            self.prog_start(total, path, unit="bytes")
-            self.index = i
-            if os.path.exists(path):
-                for piece in self.extract(path, partial):
-                    self.prog_update(len(piece))
-                    if (len(piece) == self.piece_length) or (
-                        i + 1 == len(self.paths)
-                    ):
-                        yield piece
-                    else:
-                        partial = piece
+        if self.state >= self.total:
+            fill = self.length
+        else:
+            fill = math.ceil((self.state / self.total) * self.length)
+        empty = self.length - fill
+        if self.unit == "MiB":
+            state = math.floor(self.state / 1048576)
+        elif self.unit == "KiB":
+            state = math.floor(self.state / 1024)
+        else:
+            state = self.state
+        progbar = ["[", self.fill * fill, self.empty * empty, "] ", str(state)]
+        return "".join(progbar)
+
+
+
- else: - length = self.fileinfo[i]["length"] - for pad in self._gen_padding(partial, length): - if len(pad) == self.piece_length: - yield pad - else: - partial = pad - self.prog_close() - def extract(self, path: str, partial: bytearray) -> bytearray: - """ - Split file paths contents into blocks of data for hash pieces. - Parameters - ---------- - path : str - path to content. - partial : bytes - any remaining content from last file. +
- Returns - ------- - bytearray - Hash digest for block of .torrent contents. - """ - read = 0 - length = self.fileinfo[self.index]["length"] - partial = bytearray() if len(partial) == self.piece_length else partial - if path not in self.paths: # pragma: no cover - raise MissingPathError(path) - with open(path, "rb") as current: - while True: - bitlength = self.piece_length - len(partial) - part = bytearray(bitlength) - amount = current.readinto(part) - read += amount - partial.extend(part[:amount]) - if amount < bitlength: - if amount > 0 and read == length: - self.prog_update(amount) - yield partial - break - self.prog_update(amount) - yield partial - partial = bytearray(0) - if length != read: - for pad in self._gen_padding(partial, length, read): - yield pad - def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes: - """ - Create padded pieces where file sizes do not match. - Parameters - ---------- - partial : bytes - any remaining data from last file processed. - length : int - size of space that needs padding - read : int - portion of length already padded - Yields - ------ - bytes - A piece length sized block of zeros. - """ - while read < length: - left = self.piece_length - len(partial) - if length - read > left: - padding = bytearray(left) - partial.extend(padding) - yield partial - read += left - partial = bytearray(0) - else: - partial.extend(bytearray(length - read)) - read = length - yield partial -
-
-
@@ -7848,38 +10819,78 @@

-
+
-
-__init__(self, checker) - - special - + +
+__init__(total, title, length, unit, start) +
+
-

Generate hashes of piece length data from filelist contents.

+

Construct the progress bar object and store state of it's properties.

- Source code in torrentfile\recheck.py -
def __init__(self, checker: Checker):
+          Source code in torrentfile\mixins.py
+          
69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
def __init__(self, total, title, length, unit, start):
     """
-    Generate hashes of piece length data from filelist contents.
+    Construct the progress bar object and store state of it's properties.
     """
-    self.piece_length = checker.piece_length
-    self.paths = checker.paths
-    self.pieces = checker.info["pieces"]
-    self.fileinfo = checker.fileinfo
-    self.piece_map = {}
-    self.index = 0
-    self.piece_count = 0
-    self.it = None
+    self.total = total
+    self.start = start
+    self.length = length
+    self.fill = chr(9608)
+    self.empty = chr(9617)
+    self.state = 0
+    self.unit = unit
+    self.show_total = total
+    if not unit:
+        self.unit = ""  # pragma: nocover
+    elif unit == "bytes":
+        if self.total > 10000000:
+            self.show_total = math.floor(self.total / 1048576)
+            self.unit = "MiB"
+        elif self.total > 10000:
+            self.show_total = math.floor(self.total / 1024)
+            self.unit = "KiB"
+    self.suffix = f"/{self.show_total} {self.unit}"
+    if len(title) > start:
+        title = title[: start - 1]
+    padding = (start - len(title)) * " "
+    self.prefix = "".join([title, padding])
 
+
@@ -7887,32 +10898,68 @@
-
+
-
-__iter__(self) +
+increment(value) - - special -
+
-

Assign iterator and return self.

+

Increase the state of the progress bar value.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
value + int +

the amount to increment the state by.

+ required +
- Source code in torrentfile\recheck.py -
def __iter__(self):
+          Source code in torrentfile\mixins.py
+          
 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
def increment(self, value):
     """
-    Assign iterator and return self.
+    Increase the state of the progress bar value.
+
+    Parameters
+    ----------
+    value : int
+        the amount to increment the state by.
     """
-    self.it = self.iter_pieces()
-    return self
+    self.state += value
 
+
@@ -7920,42 +10967,85 @@
-
+
-
-__next__(self) +
+pbar() - - special -
+
-

Yield back result of comparison.

+

Return the size of the filled portion of the progress bar.

+ +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
str +

the progress bar characters

- Source code in torrentfile\recheck.py -
def __next__(self):
+          Source code in torrentfile\mixins.py
+          
107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
def pbar(self):
     """
-    Yield back result of comparison.
-    """
-    try:
-        partial = next(self.it)
-    except StopIteration as itererror:
-        raise StopIteration from itererror
+    Return the size of the filled portion of the progress bar.
 
-    chunck = sha1(partial).digest()  # nosec
-    start = self.piece_count * SHA1
-    end = start + SHA1
-    piece = self.pieces[start:end]
-    self.piece_count += 1
-    path = self.paths[self.index]
-    return chunck, piece, path, len(partial)
+    Returns
+    -------
+    str :
+        the progress bar characters
+    """
+    if self.state >= self.total:
+        fill = self.length
+    else:
+        fill = math.ceil((self.state / self.total) * self.length)
+    empty = self.length - fill
+    if self.unit == "MiB":
+        state = math.floor(self.state / 1048576)
+    elif self.unit == "KiB":
+        state = math.floor(self.state / 1024)
+    else:
+        state = self.state
+    progbar = ["[", self.fill * fill, self.empty * empty, "] ", str(state)]
+    return "".join(progbar)
 
+
@@ -7963,102 +11053,159 @@
-
+
-
-extract(self, path, partial) +
+
+ + + + +
+ + + +

+waiting(msg, flag, timeout=180) + + +

-
-

Split file paths contents into blocks of data for hash pieces.

+

Show loading message while thread completes processing.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
pathstr

path to content.

required
+ - - - - + + + + - -
partialbytearray

any remaining content from last file.

requiredNameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytearray

Hash digest for block of .torrent contents.

+ + + + msg + + str + +

Message string printed before the progress bar

+ + required + + + + flag + + list + +

Once flag is filled exit loop

+ + required + + + + timeout + + int + +

max amount of time to run the function.

+ + 180 + + + + +
- Source code in torrentfile\recheck.py -
def extract(self, path: str, partial: bytearray) -> bytearray:
+          Source code in torrentfile\mixins.py
+          
213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
def waiting(msg, flag, timeout=180):
     """
-    Split file paths contents into blocks of data for hash pieces.
+    Show loading message while thread completes processing.
 
     Parameters
     ----------
-    path : str
-        path to content.
-    partial : bytes
-        any remaining content from last file.
-
-    Returns
-    -------
-    bytearray
-        Hash digest for block of .torrent contents.
+    msg : str
+        Message string printed before the progress bar
+    flag : list
+        Once flag is filled exit loop
+    timeout : int
+        max amount of time to run the function.
     """
-    read = 0
-    length = self.fileinfo[self.index]["length"]
-    partial = bytearray() if len(partial) == self.piece_length else partial
-    if path not in self.paths:  # pragma: no cover
-        raise MissingPathError(path)
-    with open(path, "rb") as current:
-        while True:
-            bitlength = self.piece_length - len(partial)
-            part = bytearray(bitlength)
-            amount = current.readinto(part)
-            read += amount
-            partial.extend(part[:amount])
-            if amount < bitlength:
-                if amount > 0 and read == length:
-                    self.prog_update(amount)
-                    yield partial
-                break
-            self.prog_update(amount)
-            yield partial
-            partial = bytearray(0)
-    if length != read:
-        for pad in self._gen_padding(partial, length, read):
-            yield pad
+    then = time.time()
+    codes, fill = list(range(9617, 9620)), chr(9619)
+    size = idx = 0
+    total = shutil.get_terminal_size().columns - len(msg) - 20
+
+    def output(text):
+        """
+        Print parameter message to the console.
+
+        Parameters
+        ----------
+        text : str
+            output message
+        """
+        sys.stdout.write(text)
+        sys.stdout.flush()
+
+    output("\n")
+    while not flag:
+        time.sleep(0.16)
+        filled = (fill * size) + chr(codes[idx]) + (" " * (total - size))
+        output(f"{msg}: {filled}\r")
+        idx = idx + 1 if idx + 1 < len(codes) else 0
+        size = size + 1 if size < total else 0
+        if time.time() - then > timeout:
+            break
+    output("\n")
 
+
@@ -8066,84 +11213,47 @@
-
+
-
-iter_pieces(self) +
+ +
+ + + +
+ + + +

+ recheck -

+ +
-

Iterate through, and hash pieces of torrent contents.

+

Module container Checker Class.

+

The CheckerClass takes a torrentfile and tha path to it's contents. +It will then iterate through every file and directory contained +and compare their data to values contained within the torrent file. +Completion percentages will be printed to screen for each file and +at the end for the torrentfile as a whole.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytes

hash digest for block of torrent data.

-
- Source code in torrentfile\recheck.py -
def iter_pieces(self):
-    """
-    Iterate through, and hash pieces of torrent contents.
 
-    Yields
-    ------
-    piece : bytes
-        hash digest for block of torrent data.
-    """
-    partial = bytearray()
-    for i, path in enumerate(self.paths):
-        total = self.fileinfo[i]["length"]
-        self.prog_start(total, path, unit="bytes")
-        self.index = i
-        if os.path.exists(path):
-            for piece in self.extract(path, partial):
-                self.prog_update(len(piece))
-                if (len(piece) == self.piece_length) or (
-                    i + 1 == len(self.paths)
-                ):
-                    yield piece
-                else:
-                    partial = piece
 
-        else:
-            length = self.fileinfo[i]["length"]
-            for pad in self._gen_padding(partial, length):
-                if len(pad) == self.piece_length:
-                    yield pad
-                else:
-                    partial = pad
-        self.prog_close()
-
-
-
+
-
-
-
-

@@ -8151,286 +11261,612 @@
-

- -HashChecker (ProgMixin) - +

+ Checker

+
+

+ Bases: ProgMixin

-

Verify that root hashes of content files match the .torrent files.

-

Parameters:

- - - - - - - - - - - - - - - - +

Check a given file or directory to see if it matches a torrentfile.

+

Public constructor for Checker class instance.

+ +

Parameters:

+
NameTypeDescriptionDefault
checkerChecker

the checker instance that maintains variables.

required
+ - - - - + + + + - -
hasherObject

the version specific hashing class for torrent content.

requiredNameTypeDescriptionDefault
-
- Source code in torrentfile\recheck.py -
class HashChecker(ProgMixin):
+    
+    
+        
+          metafile
+          
+                str
+          
+          

Path to ".torrent" file.

+ + required + + + + path + + str + +

Path where the content is located in filesystem.

+ + required + + + + +
Example
+
>> metafile = "/path/to/torrentfile/content_file_or_dir.torrent"
+>> location = "/path/to/location"
+>> os.path.exists("/path/to/location/content_file_or_dir")
+Out: True
+>> checker = Checker(metafile, location)
+
+ + +
+ Source code in torrentfile\recheck.py +
 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
class Checker(ProgMixin):
     """
-    Verify that root hashes of content files match the .torrent files.
+    Check a given file or directory to see if it matches a torrentfile.
+
+    Public constructor for Checker class instance.
 
     Parameters
     ----------
-    checker : Object
-        the checker instance that maintains variables.
-    hasher : Object
-        the version specific hashing class for torrent content.
-    """
-
-    def __init__(self, checker: Checker):
-        """
-        Construct a HybridChecker instance.
-        """
-        self.checker = checker
-        self.paths = checker.paths
-        self.piece_length = checker.piece_length
-        self.fileinfo = checker.fileinfo
-        self.piece_layers = checker.meta["piece layers"]
-        self.current = None
-        self.index = -1
+    metafile : str
+        Path to ".torrent" file.
+    path : str
+        Path where the content is located in filesystem.
 
-    def __iter__(self):
-        """
-        Assign iterator and return self.
-        """
-        return self
+    Example
+    -------
+        >> metafile = "/path/to/torrentfile/content_file_or_dir.torrent"
+        >> location = "/path/to/location"
+        >> os.path.exists("/path/to/location/content_file_or_dir")
+        Out: True
+        >> checker = Checker(metafile, location)
+    """
 
-    def __next__(self):
-        """
-        Provide the result of comparison.
-        """
-        if self.current is None:
-            self.next_file()
-        try:
-            return self.process_current()
-        except StopIteration as itererr:
-            if self.next_file():
-                return self.process_current()
-            raise StopIteration from itererr
+    _hook = None
 
-    class Padder:
+    def __init__(self, metafile: str, path: str):
         """
-        Padding class to generate padding hashes wherever needed.
+        Validate data against hashes contained in .torrent file.
 
         Parameters
         ----------
-        length: int
-            the total size of the mock file generating padding for.
-        piece_length : int
-            the block size that each hash represents.
+        metafile : str
+            path to .torrent file
+        path : str
+            path to content or contents parent directory.
         """
+        if not os.path.exists(metafile):
+            raise FileNotFoundError
+        meta = []
+        thread = Thread(target=pyben.loadinto, args=(metafile, meta))
+        thread.start()
+        self.last_log = None
+        self.log_msg("Checking: %s, %s", metafile, path)
+        self.metafile = metafile
+        self.total = 0
+        self.paths = []
+        self.fileinfo = {}
+        thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
+        if not meta:  # pragma: nocover
+            thread2.start()
+            thread2.join()
+        self.meta = meta[0]
+        self.info = self.meta["info"]
+        self.name = self.info["name"]
+        self.piece_length = self.info["piece length"]
 
-        def __init__(self, length, piece_length):
-            """
-            Construct padding class to Mock missing or incomplete files.
-            """
-            self.length = length
-            self.piece_length = piece_length
-            self.pad = sha256(bytearray(piece_length)).digest()
-
-        def __iter__(self):
-            """
-            Return self to correctly implement iterator type.
-            """
-            return self  # pragma: nocover
+        if "meta version" in self.info:
+            if "pieces" in self.info:
+                self.meta_version = 3
+            else:
+                self.meta_version = 2
+        else:
+            self.meta_version = 1
 
-        def __next__(self):
-            """
-            Iterate through seemingly endless sha256 hashes of zeros.
-            """
-            if self.length >= self.piece_length:
-                self.length -= self.piece_length
-                return self.pad
-            if self.length > 0:
-                pad = sha256(bytearray(self.length)).digest()
-                self.length -= self.length
-                return pad
-            raise StopIteration
+        self.root = self.find_root(path)
+        self.check_paths()
 
-    def next_file(self):
+    @classmethod
+    def register_callback(cls, hook):
         """
-        Remove all references to  processed files and prepare for the next.
+        Register hooks from 3rd party programs to access generated info.
+
+        Parameters
+        ----------
+        hook : function
+            callback function for the logging feature.
         """
-        self.index += 1
-        self.prog_close()
-        if self.current is None or self.index < len(self.paths):
-            self.current = self.paths[self.index]
-            self.length = self.fileinfo[self.index]["length"]
-            self.root_hash = self.fileinfo[self.index]["pieces root"]
-            if self.length > self.piece_length:
-                self.pieces = self.piece_layers[self.root_hash]
-            else:
-                self.pieces = self.root_hash
-            path = self.paths[self.index]
-            self.prog_start(self.length, path, unit="bytes")
-            self.count = 0
-            if os.path.exists(self.current):
-                self.hasher = FileHasher(path, self.piece_length, progress=0)
-            else:
-                self.hasher = self.Padder(self.length, self.piece_length)
-            return True
-        if self.index >= len(self.paths):
-            del self.current
-            del self.length
-            del self.root_hash
-            del self.pieces
-        return False
+        cls._hook = hook
 
-    def process_current(self):
+    def piece_checker(self):
         """
-        Gather necessary information to compare to metafile details.
+        Check individual pieces of the torrent.
+
+        Returns
+        -------
+        HashChecker | FeedChecker
+            Individual piece hasher.
         """
-        try:
-            layer = next(self.hasher)
-            piece, size = self.advance()
-            self.prog_update(size)
-            return layer, piece, self.current, size
-        except StopIteration as err:
-            if self.length > 0 and self.count * SHA256 < len(self.pieces):
-                self.hasher = self.Padder(self.length, self.piece_length)
-                piece, size = self.advance()
-                layer = next(self.hasher)
-                self.prog_update(0)
-                return layer, piece, self.current, size
-            raise StopIteration from err
+        if self.meta_version == 1:
+            return FeedChecker
+        return HashChecker
 
-    def advance(self):
+    def results(self):
         """
-        Increment the number of pieces processed for the current file.
+        Generate result percentage and store for future calls.
         """
-        start = self.count * SHA256
-        end = start + SHA256
-        piece = self.pieces[start:end]
-        self.count += 1
-        if self.length >= self.piece_length:
-            self.length -= self.piece_length
-            size = self.piece_length
-        else:
-            size = self.length
-            self.length -= self.length
-        return piece, size
-
- + responses = [] + for response in self.iter_hashes(): + responses.append(response) + self.log_msg( + "Final result for %s recheck: %s", self.metafile, self._result + ) + return self._result + def log_msg(self, *args, level=logging.INFO): + """ + Log message `msg` to logger and send `msg` to callback hook. -
+ Parameters + ---------- + args : dict + formatting args for log message + level : int + Log level for this message; default=`logging.INFO` + """ + message = args[0] + if len(args) >= 3: + message = message % tuple(args[1:]) + elif len(args) == 2: + message = message % args[1] + # Repeat log messages should be ignored. + if message != self.last_log: + self.last_log = message + logger.log(level, message) + if self._hook and level == logging.INFO: + self._hook(message) + def find_root(self, path: str) -> str: + """ + Check path for torrent content. + The path can be a relative or absolute filesystem path. In the case + where the content is a single file, the path may point directly to the + the file, or it may point to the parent directory. If content points + to a directory. The directory will be checked to see if it matches + the torrent's name, if not the directories contents will be searched. + The returned value will be the absolute path that matches the torrent's + name. + Parameters + ---------- + path : str + root path to torrent content + Returns + ------- + str + root path to content + """ + if not os.path.exists(path): + self.log_msg("Could not locate torrent content %s.", path) + raise FileNotFoundError(path) + root = Path(path) + if root.name == self.name: + self.log_msg("Content found: %s.", str(root)) + return root -
+ if self.name in os.listdir(root): + return root / self.name + self.log_msg("Could not locate torrent content in: %s", str(root)) + raise FileNotFoundError(root) + def check_paths(self): + """ + Gather all file paths described in the torrent file. + """ + finfo = self.fileinfo -
- -Padder + if "length" in self.info: + self.log_msg("%s points to a single file", self.root) + self.total = self.info["length"] + self.paths.append(str(self.root)) + finfo[0] = { + "path": self.root, + "length": self.info["length"], + } + if self.meta_version > 1: + root = self.info["file tree"][self.name][""]["pieces root"] + finfo[0]["pieces root"] = root -
+ return -
+ # Otherwise Content is more than 1 file. + self.log_msg("%s points to a directory", self.root) + if self.meta_version == 1: -

Padding class to generate padding hashes wherever needed.

+ for i, item in enumerate(self.info["files"]): + self.total += item["length"] + base = os.path.join(*item["path"]) -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
lengthint

the total size of the mock file generating padding for.

required
piece_lengthint

the block size that each hash represents.

required
-
- Source code in torrentfile\recheck.py -
class Padder:
-    """
-    Padding class to generate padding hashes wherever needed.
+                self.fileinfo[i] = {
+                    "path": str(self.root / base),
+                    "length": item["length"],
+                }
 
-    Parameters
-    ----------
-    length: int
-        the total size of the mock file generating padding for.
-    piece_length : int
-        the block size that each hash represents.
-    """
+                self.paths.append(str(self.root / base))
+            return
 
-    def __init__(self, length, piece_length):
-        """
-        Construct padding class to Mock missing or incomplete files.
-        """
-        self.length = length
-        self.piece_length = piece_length
-        self.pad = sha256(bytearray(piece_length)).digest()
+        self.walk_file_tree(self.info["file tree"], [])
 
-    def __iter__(self):
+    def walk_file_tree(self, tree: dict, partials: list):
         """
-        Return self to correctly implement iterator type.
-        """
-        return self  # pragma: nocover
+        Traverse File Tree dictionary to get file details.
 
-    def __next__(self):
-        """
-        Iterate through seemingly endless sha256 hashes of zeros.
+        Extract full pathnames, length, root hash, and layer hashes
+        for each file included in the .torrent's file tree.
+
+        Parameters
+        ----------
+        tree : dict
+            File Tree dict extracted from torrent file.
+        partials : list
+            list of intermediate pathnames.
         """
-        if self.length >= self.piece_length:
-            self.length -= self.piece_length
-            return self.pad
-        if self.length > 0:
-            pad = sha256(bytearray(self.length)).digest()
-            self.length -= self.length
-            return pad
-        raise StopIteration
+        for key, val in tree.items():
+
+            # Empty string means the tree's leaf is value
+            if "" in val:
+
+                base = os.path.join(*partials, key)
+                roothash = None
+                length = val[""]["length"]
+                roothash = None if not length else val[""]["pieces root"]
+                full = str(self.root / base)
+                self.fileinfo[len(self.paths)] = {
+                    "path": full,
+                    "length": length,
+                    "pieces root": roothash,
+                }
+                self.paths.append(full)
+                self.total += length
+            else:
+                self.walk_file_tree(val, partials + [key])
+
+    def iter_hashes(self) -> tuple:
+        """
+        Produce results of comparing torrent contents piece by piece.
+
+        Yields
+        ------
+        chunck : bytes
+            hash of data found on disk
+        piece : bytes
+            hash of data when complete and correct
+        path : str
+            path to file being hashed
+        size : int
+            length of bytes hashed for piece
+        """
+        matched = consumed = 0
+        checker = self.piece_checker()
+        for chunk, piece, path, size in checker(self):
+            consumed += size
+            matching = 0
+            if chunk == piece:
+                matching += size
+                matched += size
+            yield chunk, piece, path, size
+            total_consumed = str(int(consumed / self.total * 100))
+            percent_matched = str(int(matched / consumed * 100))
+            self.log_msg(
+                "Processed: %s%%, Matched: %s%%",
+                total_consumed,
+                percent_matched,
+                level=logging.DEBUG,
+            )
+        self._result = (matched / consumed) * 100 if consumed > 0 else 0
 
-
+
+
@@ -8444,65 +11880,151 @@
-
-
-__init__(self, length, piece_length) - - special - -
-
-

Construct padding class to Mock missing or incomplete files.

-
- Source code in torrentfile\recheck.py -
def __init__(self, length, piece_length):
-    """
-    Construct padding class to Mock missing or incomplete files.
-    """
-    self.length = length
-    self.piece_length = piece_length
-    self.pad = sha256(bytearray(piece_length)).digest()
-
-
-
-
+
-
+
+__init__(metafile, path) -
-__iter__(self) - - special - +
-
-

Return self to correctly implement iterator type.

+

Validate data against hashes contained in .torrent file.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
metafile + str +

path to .torrent file

+ required +
path + str +

path to content or contents parent directory.

+ required +
Source code in torrentfile\recheck.py -
def __iter__(self):
+          
 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
def __init__(self, metafile: str, path: str):
     """
-    Return self to correctly implement iterator type.
+    Validate data against hashes contained in .torrent file.
+
+    Parameters
+    ----------
+    metafile : str
+        path to .torrent file
+    path : str
+        path to content or contents parent directory.
     """
-    return self  # pragma: nocover
+    if not os.path.exists(metafile):
+        raise FileNotFoundError
+    meta = []
+    thread = Thread(target=pyben.loadinto, args=(metafile, meta))
+    thread.start()
+    self.last_log = None
+    self.log_msg("Checking: %s, %s", metafile, path)
+    self.metafile = metafile
+    self.total = 0
+    self.paths = []
+    self.fileinfo = {}
+    thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
+    if not meta:  # pragma: nocover
+        thread2.start()
+        thread2.join()
+    self.meta = meta[0]
+    self.info = self.meta["info"]
+    self.name = self.info["name"]
+    self.piece_length = self.info["piece length"]
+
+    if "meta version" in self.info:
+        if "pieces" in self.info:
+            self.meta_version = 3
+        else:
+            self.meta_version = 2
+    else:
+        self.meta_version = 1
+
+    self.root = self.find_root(path)
+    self.check_paths()
 
+
@@ -8510,88 +12032,102 @@
+
-
-__next__(self) +
+check_paths() - - special - -
+
+
-

Iterate through seemingly endless sha256 hashes of zeros.

+

Gather all file paths described in the torrent file.

Source code in torrentfile\recheck.py -
def __next__(self):
+          
213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
def check_paths(self):
     """
-    Iterate through seemingly endless sha256 hashes of zeros.
+    Gather all file paths described in the torrent file.
     """
-    if self.length >= self.piece_length:
-        self.length -= self.piece_length
-        return self.pad
-    if self.length > 0:
-        pad = sha256(bytearray(self.length)).digest()
-        self.length -= self.length
-        return pad
-    raise StopIteration
-
- - - - - - - - - - - - - - - - - - + finfo = self.fileinfo -
+ if "length" in self.info: + self.log_msg("%s points to a single file", self.root) + self.total = self.info["length"] + self.paths.append(str(self.root)) + finfo[0] = { + "path": self.root, + "length": self.info["length"], + } + if self.meta_version > 1: + root = self.info["file tree"][self.name][""]["pieces root"] + finfo[0]["pieces root"] = root -
-__init__(self, checker) + return - - special - + # Otherwise Content is more than 1 file. + self.log_msg("%s points to a directory", self.root) + if self.meta_version == 1: -
+ for i, item in enumerate(self.info["files"]): + self.total += item["length"] + base = os.path.join(*item["path"]) -
+ self.fileinfo[i] = { + "path": str(self.root / base), + "length": item["length"], + } -

Construct a HybridChecker instance.

+ self.paths.append(str(self.root / base)) + return -
- Source code in torrentfile\recheck.py -
def __init__(self, checker: Checker):
-    """
-    Construct a HybridChecker instance.
-    """
-    self.checker = checker
-    self.paths = checker.paths
-    self.piece_length = checker.piece_length
-    self.fileinfo = checker.fileinfo
-    self.piece_layers = checker.meta["piece layers"]
-    self.current = None
-    self.index = -1
+    self.walk_file_tree(self.info["file tree"], [])
 
+
@@ -8599,70 +12135,272 @@
-
+
-
-__iter__(self) +
+find_root(path) - - special -
+
-

Assign iterator and return self.

+

Check path for torrent content.

+

The path can be a relative or absolute filesystem path. In the case +where the content is a single file, the path may point directly to the +the file, or it may point to the parent directory. If content points +to a directory. The directory will be checked to see if it matches +the torrent's name, if not the directories contents will be searched. +The returned value will be the absolute path that matches the torrent's +name.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

root path to torrent content

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

root path to content

Source code in torrentfile\recheck.py -
def __iter__(self):
+          
176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
def find_root(self, path: str) -> str:
     """
-    Assign iterator and return self.
-    """
-    return self
-
- - - - - - + Check path for torrent content. -
+ The path can be a relative or absolute filesystem path. In the case + where the content is a single file, the path may point directly to the + the file, or it may point to the parent directory. If content points + to a directory. The directory will be checked to see if it matches + the torrent's name, if not the directories contents will be searched. + The returned value will be the absolute path that matches the torrent's + name. + + Parameters + ---------- + path : str + root path to torrent content + Returns + ------- + str + root path to content + """ + if not os.path.exists(path): + self.log_msg("Could not locate torrent content %s.", path) + raise FileNotFoundError(path) + root = Path(path) + if root.name == self.name: + self.log_msg("Content found: %s.", str(root)) + return root -
-__next__(self) + if self.name in os.listdir(root): + return root / self.name + + self.log_msg("Could not locate torrent content in: %s", str(root)) + raise FileNotFoundError(root) +
+
+
+
+ +
+ + + +
+ + + +
+iter_hashes() - - special -
+
-

Provide the result of comparison.

+

Produce results of comparing torrent contents piece by piece.

+ +

Yields:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Name TypeDescription
chunck + bytes +

hash of data found on disk

piece + bytes +

hash of data when complete and correct

path + str +

path to file being hashed

size + int +

length of bytes hashed for piece

Source code in torrentfile\recheck.py -
def __next__(self):
+          
287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
def iter_hashes(self) -> tuple:
     """
-    Provide the result of comparison.
+    Produce results of comparing torrent contents piece by piece.
+
+    Yields
+    ------
+    chunck : bytes
+        hash of data found on disk
+    piece : bytes
+        hash of data when complete and correct
+    path : str
+        path to file being hashed
+    size : int
+        length of bytes hashed for piece
     """
-    if self.current is None:
-        self.next_file()
-    try:
-        return self.process_current()
-    except StopIteration as itererr:
-        if self.next_file():
-            return self.process_current()
-        raise StopIteration from itererr
+    matched = consumed = 0
+    checker = self.piece_checker()
+    for chunk, piece, path, size in checker(self):
+        consumed += size
+        matching = 0
+        if chunk == piece:
+            matching += size
+            matched += size
+        yield chunk, piece, path, size
+        total_consumed = str(int(consumed / self.total * 100))
+        percent_matched = str(int(matched / consumed * 100))
+        self.log_msg(
+            "Processed: %s%%, Matched: %s%%",
+            total_consumed,
+            percent_matched,
+            level=logging.DEBUG,
+        )
+    self._result = (matched / consumed) * 100 if consumed > 0 else 0
 
+
@@ -8670,38 +12408,104 @@
-
+
-
-advance(self) +
+log_msg(*args, level=logging.INFO)
+
-

Increment the number of pieces processed for the current file.

+

Log message msg to logger and send msg to callback hook.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + dict +

formatting args for log message

+ required +
level + int +

Log level for this message; default=logging.INFO

+ logging.INFO +
Source code in torrentfile\recheck.py -
def advance(self):
+          
152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
def log_msg(self, *args, level=logging.INFO):
     """
-    Increment the number of pieces processed for the current file.
+    Log message `msg` to logger and send `msg` to callback hook.
+
+    Parameters
+    ----------
+    args : dict
+        formatting args for log message
+    level : int
+        Log level for this message; default=`logging.INFO`
     """
-    start = self.count * SHA256
-    end = start + SHA256
-    piece = self.pieces[start:end]
-    self.count += 1
-    if self.length >= self.piece_length:
-        self.length -= self.piece_length
-        size = self.piece_length
-    else:
-        size = self.length
-        self.length -= self.length
-    return piece, size
+    message = args[0]
+    if len(args) >= 3:
+        message = message % tuple(args[1:])
+    elif len(args) == 2:
+        message = message % args[1]
+
+    # Repeat log messages should be ignored.
+    if message != self.last_log:
+        self.last_log = message
+        logger.log(level, message)
+        if self._hook and level == logging.INFO:
+            self._hook(message)
 
+
@@ -8709,51 +12513,66 @@
-
+
-
-next_file(self) +
+piece_checker()
+
-

Remove all references to processed files and prepare for the next.

+

Check individual pieces of the torrent.

+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ HashChecker | FeedChecker +

Individual piece hasher.

Source code in torrentfile\recheck.py -
def next_file(self):
+          
126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
def piece_checker(self):
     """
-    Remove all references to  processed files and prepare for the next.
+    Check individual pieces of the torrent.
+
+    Returns
+    -------
+    HashChecker | FeedChecker
+        Individual piece hasher.
     """
-    self.index += 1
-    self.prog_close()
-    if self.current is None or self.index < len(self.paths):
-        self.current = self.paths[self.index]
-        self.length = self.fileinfo[self.index]["length"]
-        self.root_hash = self.fileinfo[self.index]["pieces root"]
-        if self.length > self.piece_length:
-            self.pieces = self.piece_layers[self.root_hash]
-        else:
-            self.pieces = self.root_hash
-        path = self.paths[self.index]
-        self.prog_start(self.length, path, unit="bytes")
-        self.count = 0
-        if os.path.exists(self.current):
-            self.hasher = FileHasher(path, self.piece_length, progress=0)
-        else:
-            self.hasher = self.Padder(self.length, self.piece_length)
-        return True
-    if self.index >= len(self.paths):
-        del self.current
-        del self.length
-        del self.root_hash
-        del self.pieces
-    return False
+    if self.meta_version == 1:
+        return FeedChecker
+    return HashChecker
 
+
@@ -8761,40 +12580,73 @@
-
+
-
-process_current(self) +
+register_callback(hook) + + classmethod +
+
-

Gather necessary information to compare to metafile details.

+

Register hooks from 3rd party programs to access generated info.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
hook + function +

callback function for the logging feature.

+ required +
Source code in torrentfile\recheck.py -
def process_current(self):
+          
114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
@classmethod
+def register_callback(cls, hook):
     """
-    Gather necessary information to compare to metafile details.
+    Register hooks from 3rd party programs to access generated info.
+
+    Parameters
+    ----------
+    hook : function
+        callback function for the logging feature.
     """
-    try:
-        layer = next(self.hasher)
-        piece, size = self.advance()
-        self.prog_update(size)
-        return layer, piece, self.current, size
-    except StopIteration as err:
-        if self.length > 0 and self.count * SHA256 < len(self.pieces):
-            self.hasher = self.Padder(self.length, self.piece_length)
-            piece, size = self.advance()
-            layer = next(self.hasher)
-            self.prog_update(0)
-            return layer, piece, self.current, size
-        raise StopIteration from err
+    cls._hook = hook
 
+
@@ -8802,247 +12654,189 @@
-
- -
- -
+
+results() +
+
+

Generate result percentage and store for future calls.

-
+
+ Source code in torrentfile\recheck.py +
139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
def results(self):
+    """
+    Generate result percentage and store for future calls.
+    """
+    responses = []
+    for response in self.iter_hashes():
+        responses.append(response)
 
+    self.log_msg(
+        "Final result for %s recheck:  %s", self.metafile, self._result
+    )
+    return self._result
+
+
+
-
+
-

- torrent +

+walk_file_tree(tree, partials) +
-
-

Classes and procedures pertaining to the creation of torrent meta files.

-
Classes
-
    -
  • -

    TorrentFile - construct .torrent file.

    -
  • -
  • -

    TorrentFileV2 - construct .torrent v2 files using provided data.

    -
  • -
  • -

    MetaFile - base class for all MetaFile classes.

    -
  • -
-
Constants
-
    -
  • -

    BLOCK_SIZE : int - size of leaf hashes for merkle tree.

    -
  • -
  • -

    HASH_SIZE : int - Length of a sha256 hash.

    -
  • -
-
Bittorrent V2
-

From Bittorrent.org Documentation pages.

-

Implementation details for Bittorrent Protocol v2.

-
-

Note

-

All strings in a .torrent file that contain text must be UTF-8 encoded.

-
-
Meta Version 2 Dictionary:
-
    -
  • -

    "announce": - The URL of the tracker.

    -
  • -
  • -

    "info": - This maps to a dictionary, with keys described below.

    -
      -
    • -

      "name": - A display name for the torrent. It is purely advisory.

      -
    • -
    • -

      "piece length": - The number of bytes that each logical piece in the peer - protocol refers to. I.e. it sets the granularity of piece, request, - bitfield and have messages. It must be a power of two and at least - 6KiB.

      -
    • -
    • -

      "meta version": - An integer value, set to 2 to indicate compatibility - with the current revision of this specification. Version 1 is not - assigned to avoid confusion with BEP3. Future revisions will only - increment this issue to indicate an incompatible change has been made, - for example that hash algorithms were changed due to newly discovered - vulnerabilities. Lementations must check this field first and indicate - that a torrent is of a newer version than they can handle before - performing other idations which may result in more general messages - about invalid files. Files are mapped into this piece address space so - that each non-empty

      -
    • -
    • -

      "file tree": - A tree of dictionaries where dictionary keys represent UTF-8 - encoded path elements. Entries with zero-length keys describe the - properties of the composed path at that point. 'UTF-8 encoded' - context only means that if the native encoding is known at creation - time it must be converted to UTF-8. Keys may contain invalid UTF-8 - sequences or characters and names that are reserved on specific - filesystems. Implementations must be prepared to sanitize them. On - platforms path components exactly matching '.' and '..' must be - sanitized since they could lead to directory traversal attacks and - conflicting path descriptions. On platforms that require UTF-8 - path components this sanitizing step must happen after normalizing - overlong UTF-8 encodings. - File is aligned to a piece boundary and occurs in same order as - the file tree. The last piece of each file may be shorter than the - specified piece length, resulting in an alignment gap.

      -
    • -
    • -

      "length": - Length of the file in bytes. Presence of this field indicates - that the dictionary describes a file, not a directory. Which means - it must not have any sibling entries.

      -
    • -
    • -

      "pieces root": - For non-empty files this is the the root hash of a merkle - tree with a branching factor of 2, constructed from 16KiB blocks - of the file. The last block may be shorter than 16KiB. The - remaining leaf hashes beyond the end of the file required to - construct upper layers of the merkle tree are set to zero. As of - meta version 2 SHA2-256 is used as digest function for the merkle - tree. The hash is stored in its binary form, not as human-readable - string.

      -
    • -
    -
  • -
  • -

    "piece layers": - A dictionary of strings. For each file in the file tree that - is larger than the piece size it contains one string value. - The keys are the merkle roots while the values consist of concatenated - hashes of one layer within that merkle tree. The layer is chosen so - that one hash covers piece length bytes. For example if the piece - size is 16KiB then the leaf hashes are used. If a piece size of - 128KiB is used then 3rd layer up from the leaf hashes is used. Layer - hashes which exclusively cover data beyond the end of file, i.e. - are only needed to balance the tree, are omitted. All hashes are - stored in their binary format. A torrent is not valid if this field is - absent, the contained hashes do not match the merkle roots or are - not from the correct layer.

    -
  • -
-
-

Important

-

The file tree root dictionary itself must not be a file, -i.e. it must not contain a zero-length key with a dictionary containing -a length key.

-
-
Bittorrent V1
-
v1 meta-dictionary
-
    -
  • -

    announce: - The URL of the tracker.

    -
  • -
  • -

    info: - This maps to a dictionary, with keys described below.

    -
      -
    • -

      name: - maps to a UTF-8 encoded string which is the suggested name to - save the file (or directory) as. It is purely advisory.

      -
    • -
    • -

      piece length: - maps to the number of bytes in each piece the file is split - into. For the purposes of transfer, files are split into - fixed-size pieces which are all the same length except for - possibly the last one which may be truncated.

      -
    • -
    • -

      piece length: - is almost always a power of two, most commonly 2^18 = 256 K

      -
    • -
    • -

      pieces: - maps to a string whose length is a multiple of 20. It is to be - subdivided into strings of length 20, each of which is the SHA1 - hash of the piece at the corresponding index.

      -
    • -
    • -

      length: - In the single file case, maps to the length of the file in bytes.

      -
    • -
    • -

      files: - If present then the download represents a single file, otherwise it - represents a set of files which go in a directory structure. - For the purposes of the other keys, the multi-file case is treated - as only having a single file by concatenating the files in the order - they appear in the files list. The files list is the value files - maps to, and is a list of dictionaries containing the following keys:

      -
        -
      • -

        path: - A list of UTF-8 encoded strings corresponding to subdirectory - names, the last of which is the actual file name

        -
      • -
      • -

        length: - Maps to the length of the file in bytes.

        -
      • -
      -
    • -
    • -

      length: - Only present if the content is a single file. Maps to the length - of the file in bytes.

      -
    • -
    -
  • -
-
-

Note

-

In the single file case, the name key is the name of a file, -in the muliple file case, it's the name of a directory.

-
+

Traverse File Tree dictionary to get file details.

+

Extract full pathnames, length, root hash, and layer hashes +for each file included in the .torrent's file tree.

+

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
tree + dict +

File Tree dict extracted from torrent file.

+ required +
partials + list +

list of intermediate pathnames.

+ required +
+
+ Source code in torrentfile\recheck.py +
253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
def walk_file_tree(self, tree: dict, partials: list):
+    """
+    Traverse File Tree dictionary to get file details.
+
+    Extract full pathnames, length, root hash, and layer hashes
+    for each file included in the .torrent's file tree.
+
+    Parameters
+    ----------
+    tree : dict
+        File Tree dict extracted from torrent file.
+    partials : list
+        list of intermediate pathnames.
+    """
+    for key, val in tree.items():
+
+        # Empty string means the tree's leaf is value
+        if "" in val:
+
+            base = os.path.join(*partials, key)
+            roothash = None
+            length = val[""]["length"]
+            roothash = None if not length else val[""]["pieces root"]
+            full = str(self.root / base)
+            self.fileinfo[len(self.paths)] = {
+                "path": full,
+                "length": length,
+                "pieces root": roothash,
+            }
+            self.paths.append(full)
+            self.total += length
+        else:
+            self.walk_file_tree(val, partials + [key])
+
+
+
+
+ +
-
+
+ +
+
@@ -9050,491 +12844,434 @@
v1 meta-dictionary
-

- -MetaFile +

+ FeedChecker

+
+

+ Bases: ProgMixin

-

Base Class for all TorrentFile classes.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

Validates torrent content.

+

Seemlesly validate torrent file contents by comparing hashes in +metafile against data on disk.

+ +

Parameters:

+
NameTypeDescriptionDefault
pathstr

target path to torrent content. Default: None

None
announcestr

One or more tracker URL's. Default: None

None
commentstr

A comment. Default: None

None
piece_lengthint

Size of torrent pieces. Default: None

None
privatebool

For private trackers. Default: None

False
outfilestr

target path to write .torrent file. Default: None

None
sourcestr

Private tracker source. Default: None

None
progressstr

level of progress bar displayed Default: "1"

1
cwdbool

If True change default save location to current directory

False
+ - - - - + + + + - - - - - - - - - - - - - - - - - - - -
httpseedslist

one or more web addresses where torrent content can be found.

NoneNameTypeDescriptionDefault
url_listlist

one or more web addressess where torrent content exists.

None
contentstr

alias for 'path' arg.

None
meta_versionint

indicates which Bittorrent protocol to use for hashing content

None
-
- Source code in torrentfile\torrent.py -
class MetaFile:
+    
+    
+        
+          checker
+          
+                object
+          
+          

the checker class instance.

+ + required + + + + hasher + + Any + +

hashing class for calculating piece hashes. default=None

+ + required + + + + + + +
+ Source code in torrentfile\recheck.py +
322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
class FeedChecker(ProgMixin):
     """
-    Base Class for all TorrentFile classes.
+    Validates torrent content.
+
+    Seemlesly validate torrent file contents by comparing hashes in
+    metafile against data on disk.
 
     Parameters
     ----------
-    path : str
-        target path to torrent content.  Default: None
-    announce : str
-        One or more tracker URL's.  Default: None
-    comment : str
-        A comment.  Default: None
-    piece_length : int
-        Size of torrent pieces.  Default: None
-    private : bool
-        For private trackers.  Default: None
-    outfile : str
-        target path to write .torrent file. Default: None
-    source : str
-        Private tracker source. Default: None
-    progress : str
-        level of progress bar displayed  Default: "1"
-    cwd : bool
-        If True change default save location to current directory
-    httpseeds : list
-        one or more web addresses where torrent content can be found.
-    url_list : list
-        one or more web addressess where torrent content exists.
-    content : str
-        alias for 'path' arg.
-    meta_version : int
-        indicates which Bittorrent protocol to use for hashing content
+    checker : object
+        the checker class instance.
+    hasher : Any
+        hashing class for calculating piece hashes. default=None
     """
 
-    hasher = None
-
-    @classmethod
-    def set_callback(cls, func):
+    def __init__(self, checker: Checker):
         """
-        Assign a callback function for the Hashing class to call for each hash.
-
-        Parameters
-        ----------
-        func : function
-            The callback function which accepts a single paramter.
+        Generate hashes of piece length data from filelist contents.
         """
-        if "hasher" in vars(cls) and vars(cls)["hasher"]:
-            cls.hasher.set_callback(func)
+        self.piece_length = checker.piece_length
+        self.paths = checker.paths
+        self.pieces = checker.info["pieces"]
+        self.fileinfo = checker.fileinfo
+        self.piece_map = {}
+        self.index = 0
+        self.piece_count = 0
+        self.it = None
 
-    def __init__(
-        self,
-        path=None,
-        announce=None,
-        comment=None,
-        piece_length=None,
-        private=False,
-        outfile=None,
-        source=None,
-        progress=1,
-        cwd=False,
-        httpseeds=None,
-        url_list=None,
-        content=None,
-        meta_version=None,
-        **_,
-    ):
+    def __iter__(self):
         """
-        Construct MetaFile superclass and assign local attributes.
+        Assign iterator and return self.
         """
-        self.private = private
-        self.cwd = cwd
-        self.outfile = outfile
-        self.progress = int(progress)
-        self.comment = comment
-        self.source = source
-        self.meta_version = meta_version
-
-        if content:
-            path = content
-        if not path:
-            if announce and len(announce) > 1 and os.path.exists(announce[-1]):
-                path = announce[-1]
-                announce = announce[:-1]
-            elif url_list and os.path.exists(url_list[-1]):
-                path = url_list[-1]
-                url_list = url_list[:-1]
-            elif httpseeds and os.path.exists(httpseeds[-1]):
-                path = httpseeds[-1]
-                httpseeds = httpseeds[:-1]
-            else:
-                raise utils.MissingPathError("Path to content is required.")
-
-        # base path to torrent content.
-        self.path = path
-
-        logger.debug("path parameter found %s", path)
-
-        # Format piece_length attribute.
-        if piece_length:
-            self.piece_length = utils.normalize_piece_length(piece_length)
-            logger.debug("piece length parameter found %s", piece_length)
-        else:
-            self.piece_length = utils.path_piece_length(self.path)
-            logger.debug("piece length calculated %s", self.piece_length)
-
-        # Assign announce URL to empty string if none provided.
-        if not announce:
-            self.announce, self.announce_list = "", [[""]]
-
-        # Most torrent clients have editting trackers as a feature.
-        elif isinstance(announce, str):
-            self.announce, self.announce_list = announce, [[announce]]
-
-        elif isinstance(announce, Sequence):
-            self.announce, self.announce_list = announce[0], [announce]
+        self.it = self.iter_pieces()
+        return self
 
-        self.meta = {
-            "announce": self.announce,
-            "announce-list": self.announce_list,
-            "created by": f"TorrentFile:v{version}",
-            "creation date": int(datetime.timestamp(datetime.now())),
-            "info": {},
-        }
-        if comment:
-            self.meta["info"]["comment"] = comment
-            logger.debug("comment parameter found %s", comment)
-        if private:
-            self.meta["info"]["private"] = 1
-            logger.debug("private parameter triggered")
-        if source:
-            self.meta["info"]["source"] = source
-            logger.debug("source parameter found %s", source)
-        if url_list:
-            self.meta["url-list"] = url_list
-            logger.debug("url list parameter found %s", str(url_list))
-        if httpseeds:
-            self.meta["httpseeds"] = httpseeds
-            logger.debug("httpseeds parameter found %s", str(httpseeds))
-        self.meta["info"]["piece length"] = self.piece_length
+    def __next__(self):
+        """
+        Yield back result of comparison.
+        """
+        try:
+            partial = next(self.it)
+        except StopIteration as itererror:
+            raise StopIteration from itererror
 
-        self.meta_version = meta_version
-        parent, self.name = os.path.split(self.path)
-        if not self.name:
-            self.name = os.path.basename(parent)
-        self.meta["info"]["name"] = self.name
+        chunck = sha1(partial).digest()  # nosec
+        start = self.piece_count * SHA1
+        end = start + SHA1
+        piece = self.pieces[start:end]
+        self.piece_count += 1
+        path = self.paths[self.index]
+        return chunck, piece, path, len(partial)
 
-    def assemble(self):
+    def iter_pieces(self):
         """
-        Overload in subclasses.
+        Iterate through, and hash pieces of torrent contents.
 
-        Raises
+        Yields
         ------
-        Exception
-            NotImplementedError
+        piece : bytes
+            hash digest for block of torrent data.
         """
-        raise NotImplementedError
+        partial = bytearray()
+        for i, path in enumerate(self.paths):
+            total = self.fileinfo[i]["length"]
+            self.prog_start(total, path, unit="bytes")
+            self.index = i
+            if os.path.exists(path):
+                for piece in self.extract(path, partial):
+                    self.prog_update(len(piece))
+                    if (len(piece) == self.piece_length) or (
+                        i + 1 == len(self.paths)
+                    ):
+                        yield piece
+                    else:
+                        partial = piece
 
-    def sort_meta(self):
-        """Sort the info and meta dictionaries."""
-        logger.debug("sorting dictionary keys")
-        meta = self.meta
-        meta["info"] = dict(sorted(list(meta["info"].items())))
-        meta = dict(sorted(list(meta.items())))
-        return meta
+            else:
+                length = self.fileinfo[i]["length"]
+                for pad in self._gen_padding(partial, length):
+                    if len(pad) == self.piece_length:
+                        yield pad
+                    else:
+                        partial = pad
+            self.prog_close()
 
-    def write(self, outfile=None) -> tuple:
+    def extract(self, path: str, partial: bytearray) -> bytearray:
         """
-        Write meta information to .torrent file.
+        Split file paths contents into blocks of data for hash pieces.
 
         Parameters
         ----------
-        outfile : str
-            Destination path for .torrent file. default=None
+        path : str
+            path to content.
+        partial : bytes
+            any remaining content from last file.
 
         Returns
         -------
-        outfile : str
-            Where the .torrent file was writen.
-        meta : dict
-            .torrent meta information.
+        bytearray
+            Hash digest for block of .torrent contents.
         """
-        fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
-        if not self.outfile and not outfile:
-            if self.cwd:
-                self.outfile = fallback
-            else:
-                path = str(self.path).rstrip("\\/")
-                self.outfile = path + ".torrent"
-        elif outfile:
-            self.outfile = outfile
-        if str(self.outfile)[-1] in "\\/":
-            self.outfile = self.outfile + (self.name + ".torrent")
-        self.meta = self.sort_meta()
-        try:
-            pyben.dump(self.meta, self.outfile)
-        except PermissionError:
-            self.outfile = fallback
-            pyben.dump(self.meta, fallback)
-        return self.outfile, self.meta
-
- - - - -
- - - - - - - - - + read = 0 + length = self.fileinfo[self.index]["length"] + partial = bytearray() if len(partial) == self.piece_length else partial + if path not in self.paths: # pragma: no cover + raise MissingPathError(path) + with open(path, "rb") as current: + while True: + bitlength = self.piece_length - len(partial) + part = bytearray(bitlength) + amount = current.readinto(part) + read += amount + partial.extend(part[:amount]) + if amount < bitlength: + if amount > 0 and read == length: + self.prog_update(amount) + yield partial + break + self.prog_update(amount) + yield partial + partial = bytearray(0) + if length != read: + for pad in self._gen_padding(partial, length, read): + yield pad -
+ def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes: + """ + Create padded pieces where file sizes do not match. + Parameters + ---------- + partial : bytes + any remaining data from last file processed. + length : int + size of space that needs padding + read : int + portion of length already padded + Yields + ------ + bytes + A piece length sized block of zeros. + """ + while read < length: + left = self.piece_length - len(partial) + if length - read > left: + padding = bytearray(left) + partial.extend(padding) + yield partial + read += left + partial = bytearray(0) + else: + partial.extend(bytearray(length - read)) + read = length + yield partial +
+
+
-
-__init__(self, path=None, announce=None, comment=None, piece_length=None, private=False, outfile=None, source=None, progress=1, cwd=False, httpseeds=None, url_list=None, content=None, meta_version=None, **_) - - special - -
+
-
-

Construct MetaFile superclass and assign local attributes.

-
- Source code in torrentfile\torrent.py -
def __init__(
-    self,
-    path=None,
-    announce=None,
-    comment=None,
-    piece_length=None,
-    private=False,
-    outfile=None,
-    source=None,
-    progress=1,
-    cwd=False,
-    httpseeds=None,
-    url_list=None,
-    content=None,
-    meta_version=None,
-    **_,
-):
-    """
-    Construct MetaFile superclass and assign local attributes.
-    """
-    self.private = private
-    self.cwd = cwd
-    self.outfile = outfile
-    self.progress = int(progress)
-    self.comment = comment
-    self.source = source
-    self.meta_version = meta_version
 
-    if content:
-        path = content
-    if not path:
-        if announce and len(announce) > 1 and os.path.exists(announce[-1]):
-            path = announce[-1]
-            announce = announce[:-1]
-        elif url_list and os.path.exists(url_list[-1]):
-            path = url_list[-1]
-            url_list = url_list[:-1]
-        elif httpseeds and os.path.exists(httpseeds[-1]):
-            path = httpseeds[-1]
-            httpseeds = httpseeds[:-1]
-        else:
-            raise utils.MissingPathError("Path to content is required.")
 
-    # base path to torrent content.
-    self.path = path
 
-    logger.debug("path parameter found %s", path)
 
-    # Format piece_length attribute.
-    if piece_length:
-        self.piece_length = utils.normalize_piece_length(piece_length)
-        logger.debug("piece length parameter found %s", piece_length)
-    else:
-        self.piece_length = utils.path_piece_length(self.path)
-        logger.debug("piece length calculated %s", self.piece_length)
 
-    # Assign announce URL to empty string if none provided.
-    if not announce:
-        self.announce, self.announce_list = "", [[""]]
 
-    # Most torrent clients have editting trackers as a feature.
-    elif isinstance(announce, str):
-        self.announce, self.announce_list = announce, [[announce]]
 
-    elif isinstance(announce, Sequence):
-        self.announce, self.announce_list = announce[0], [announce]
 
-    self.meta = {
-        "announce": self.announce,
-        "announce-list": self.announce_list,
-        "created by": f"TorrentFile:v{version}",
-        "creation date": int(datetime.timestamp(datetime.now())),
-        "info": {},
-    }
-    if comment:
-        self.meta["info"]["comment"] = comment
-        logger.debug("comment parameter found %s", comment)
-    if private:
-        self.meta["info"]["private"] = 1
-        logger.debug("private parameter triggered")
-    if source:
-        self.meta["info"]["source"] = source
-        logger.debug("source parameter found %s", source)
-    if url_list:
-        self.meta["url-list"] = url_list
-        logger.debug("url list parameter found %s", str(url_list))
-    if httpseeds:
-        self.meta["httpseeds"] = httpseeds
-        logger.debug("httpseeds parameter found %s", str(httpseeds))
-    self.meta["info"]["piece length"] = self.piece_length
 
-    self.meta_version = meta_version
-    parent, self.name = os.path.split(self.path)
-    if not self.name:
-        self.name = os.path.basename(parent)
-    self.meta["info"]["name"] = self.name
-
-
-
-
-
+
-
-assemble(self) +
+__init__(checker)
+
-

Overload in subclasses.

+

Generate hashes of piece length data from filelist contents.

-

Exceptions:

- - - - - - - - - - - - - -
TypeDescription
Exception

NotImplementedError

- Source code in torrentfile\torrent.py -
def assemble(self):
+          Source code in torrentfile\recheck.py
+          
337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
def __init__(self, checker: Checker):
     """
-    Overload in subclasses.
-
-    Raises
-    ------
-    Exception
-        NotImplementedError
+    Generate hashes of piece length data from filelist contents.
     """
-    raise NotImplementedError
+    self.piece_length = checker.piece_length
+    self.paths = checker.paths
+    self.pieces = checker.info["pieces"]
+    self.fileinfo = checker.fileinfo
+    self.piece_map = {}
+    self.index = 0
+    self.piece_count = 0
+    self.it = None
 
+
@@ -9542,57 +13279,36 @@
-
+
-
-set_callback(func) +
+__iter__() - - classmethod -
+
-

Assign a callback function for the Hashing class to call for each hash.

+

Assign iterator and return self.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
funcfunction

The callback function which accepts a single paramter.

required
- Source code in torrentfile\torrent.py -
@classmethod
-def set_callback(cls, func):
+          Source code in torrentfile\recheck.py
+          
350
+351
+352
+353
+354
+355
def __iter__(self):
     """
-    Assign a callback function for the Hashing class to call for each hash.
-
-    Parameters
-    ----------
-    func : function
-        The callback function which accepts a single paramter.
+    Assign iterator and return self.
     """
-    if "hasher" in vars(cls) and vars(cls)["hasher"]:
-        cls.hasher.set_callback(func)
+    self.it = self.iter_pieces()
+    return self
 
+
@@ -9600,30 +13316,56 @@
-
+
-
-sort_meta(self) +
+__next__()
+
-

Sort the info and meta dictionaries.

+

Yield back result of comparison.

- Source code in torrentfile\torrent.py -
def sort_meta(self):
-    """Sort the info and meta dictionaries."""
-    logger.debug("sorting dictionary keys")
-    meta = self.meta
-    meta["info"] = dict(sorted(list(meta["info"].items())))
-    meta = dict(sorted(list(meta.items())))
-    return meta
+          Source code in torrentfile\recheck.py
+          
357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
def __next__(self):
+    """
+    Yield back result of comparison.
+    """
+    try:
+        partial = next(self.it)
+    except StopIteration as itererror:
+        raise StopIteration from itererror
+
+    chunck = sha1(partial).digest()  # nosec
+    start = self.piece_count * SHA1
+    end = start + SHA1
+    piece = self.pieces[start:end]
+    self.piece_count += 1
+    path = self.paths[self.index]
+    return chunck, piece, path, len(partial)
 
+
@@ -9631,91 +13373,146 @@
-
+
-
-write(self, outfile=None) +
+_gen_padding(partial, length, read=0)
+
-

Write meta information to .torrent file.

+

Create padded pieces where file sizes do not match.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
outfilestr

Destination path for .torrent file. default=None

NoneNameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
tuple

Where the .torrent file was writen.

+ + + + partial + + bytes + +

any remaining data from last file processed.

+ + required + + + + length + + int + +

size of space that needs padding

+ + required + + + + read + + int + +

portion of length already padded

+ + 0 + + + + + +

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ bytes +

A piece length sized block of zeros.

+
- Source code in torrentfile\torrent.py -
def write(self, outfile=None) -> tuple:
-    """
-    Write meta information to .torrent file.
+          Source code in torrentfile\recheck.py
+          
447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
+    """
+    Create padded pieces where file sizes do not match.
 
     Parameters
     ----------
-    outfile : str
-        Destination path for .torrent file. default=None
+    partial : bytes
+        any remaining data from last file processed.
+    length : int
+        size of space that needs padding
+    read : int
+        portion of length already padded
 
-    Returns
-    -------
-    outfile : str
-        Where the .torrent file was writen.
-    meta : dict
-        .torrent meta information.
+    Yields
+    ------
+    bytes
+        A piece length sized block of zeros.
     """
-    fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
-    if not self.outfile and not outfile:
-        if self.cwd:
-            self.outfile = fallback
+    while read < length:
+        left = self.piece_length - len(partial)
+        if length - read > left:
+            padding = bytearray(left)
+            partial.extend(padding)
+            yield partial
+            read += left
+            partial = bytearray(0)
         else:
-            path = str(self.path).rstrip("\\/")
-            self.outfile = path + ".torrent"
-    elif outfile:
-        self.outfile = outfile
-    if str(self.outfile)[-1] in "\\/":
-        self.outfile = self.outfile + (self.name + ".torrent")
-    self.meta = self.sort_meta()
-    try:
-        pyben.dump(self.meta, self.outfile)
-    except PermissionError:
-        self.outfile = fallback
-        pyben.dump(self.meta, fallback)
-    return self.outfile, self.meta
+            partial.extend(bytearray(length - read))
+            read = length
+            yield partial
 
+
@@ -9723,163 +13520,275 @@
- - -
- -
- -
- - - -
+
-

- -TorrentAssembler (MetaFile) - +

+extract(path, partial) +
-
-

Assembler class for Bittorrent version 2 and hybrid meta files.

-

This differs from the TorrentFileV2 and TorrentFileHybrid, because -it can be used as an iterator and works for both versions.

+

Split file paths contents into blocks of data for hash pieces.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
kwargsdict

Keyword arguments for torrent options.

{}NameTypeDescriptionDefault
+ + + + path + + str + +

path to content.

+ + required + + + + partial + + bytes + +

any remaining content from last file.

+ + required + + + + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ bytearray +

Hash digest for block of .torrent contents.

+
- Source code in torrentfile\torrent.py -
class TorrentAssembler(MetaFile):
+          Source code in torrentfile\recheck.py
+          
407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
def extract(self, path: str, partial: bytearray) -> bytearray:
     """
-    Assembler class for Bittorrent version 2 and hybrid meta files.
-
-    This differs from the TorrentFileV2 and TorrentFileHybrid, because
-    it can be used as an iterator and works for both versions.
+    Split file paths contents into blocks of data for hash pieces.
 
     Parameters
     ----------
-    kwargs : dict
-        Keyword arguments for torrent options.
+    path : str
+        path to content.
+    partial : bytes
+        any remaining content from last file.
+
+    Returns
+    -------
+    bytearray
+        Hash digest for block of .torrent contents.
     """
+    read = 0
+    length = self.fileinfo[self.index]["length"]
+    partial = bytearray() if len(partial) == self.piece_length else partial
+    if path not in self.paths:  # pragma: no cover
+        raise MissingPathError(path)
+    with open(path, "rb") as current:
+        while True:
+            bitlength = self.piece_length - len(partial)
+            part = bytearray(bitlength)
+            amount = current.readinto(part)
+            read += amount
+            partial.extend(part[:amount])
+            if amount < bitlength:
+                if amount > 0 and read == length:
+                    self.prog_update(amount)
+                    yield partial
+                break
+            self.prog_update(amount)
+            yield partial
+            partial = bytearray(0)
+    if length != read:
+        for pad in self._gen_padding(partial, length, read):
+            yield pad
+
+
+
+
- hasher = FileHasher +
- def __init__(self, **kwargs): - """ - Create Bittorrent v1 v2 hybrid metafiles. - """ - super().__init__(**kwargs) - logger.debug("Assembling bittorrent Hybrid file") - self.name = os.path.basename(self.path) - self.hashes = [] - self.piece_layers = {} - self.pieces = bytearray() - self.files = [] - self.hybrid = self.meta_version == "3" - self.total = len(utils.get_file_list(self.path)) - self.assemble() - def assemble(self): - """ - Assemble the parts of the torrentfile into meta dictionary. - """ - info = self.meta["info"] - info["meta version"] = 2 - if os.path.isfile(self.path): - info["file tree"] = {self.name: self._traverse(self.path)} - info["length"] = os.path.getsize(self.path) +
- else: - info["file tree"] = self._traverse(self.path) - if self.hybrid: - info["files"] = self.files - if self.hybrid: - info["pieces"] = self.pieces - self.meta["piece layers"] = self.piece_layers - return info - def _traverse(self, path: str) -> dict: - """ - Build meta dictionary while walking directory. +
+iter_pieces() - Parameters - ---------- - path : str - Path to target file. - """ - if os.path.isfile(path): - file_size = os.path.getsize(path) - if self.hybrid: - self.files.append( - { - "length": file_size, - "path": os.path.relpath(path, self.path).split(os.sep), - } - ) - if file_size == 0: - return {"": {"length": file_size}} +
- logger.debug("Hashing %s", str(path)) - hasher = FileHasher( - path, self.piece_length, progress=True, hybrid=self.hybrid - ) - layers = bytearray() - for result in hasher: - if self.hybrid: - layer_hash, piece = result - self.pieces.extend(piece) - else: - layer_hash = result - layers.extend(layer_hash) - if file_size > self.piece_length: - self.piece_layers[hasher.root] = layers - if self.hybrid and hasher.padding_file: - self.files.append(hasher.padding_file) - return {"": {"length": file_size, "pieces root": hasher.root}} +
- tree = {} - if os.path.isdir(path): - for name in sorted(os.listdir(path)): - tree[name] = self._traverse(os.path.join(path, name)) - return tree +

Iterate through, and hash pieces of torrent contents.

+ +

Yields:

+ + + + + + + + + + + + + +
Name TypeDescription
piece + bytes +

hash digest for block of torrent data.

+ +
+ Source code in torrentfile\recheck.py +
374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
def iter_pieces(self):
+    """
+    Iterate through, and hash pieces of torrent contents.
+
+    Yields
+    ------
+    piece : bytes
+        hash digest for block of torrent data.
+    """
+    partial = bytearray()
+    for i, path in enumerate(self.paths):
+        total = self.fileinfo[i]["length"]
+        self.prog_start(total, path, unit="bytes")
+        self.index = i
+        if os.path.exists(path):
+            for piece in self.extract(path, partial):
+                self.prog_update(len(piece))
+                if (len(piece) == self.piece_length) or (
+                    i + 1 == len(self.paths)
+                ):
+                    yield piece
+                else:
+                    partial = piece
+
+        else:
+            length = self.fileinfo[i]["length"]
+            for pad in self._gen_padding(partial, length):
+                if len(pad) == self.piece_length:
+                    yield pad
+                else:
+                    partial = pad
+        self.prog_close()
 
+
+
+
-
+
+ +
+
@@ -9887,193 +13796,418 @@

-

- -hasher (CbMixin, ProgMixin) - +

+ HashChecker -

+
+
+

+ Bases: ProgMixin

-

Calculate root and piece hashes for creating hybrid torrent file.

-

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. -With a branching factor of 2, merge layer hashes until blocks equal -piece_length bytes for the piece layer, and then the root hash.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - +

Verify that root hashes of content files match the .torrent files.

+ +

Parameters:

+
NameTypeDescriptionDefault
pathstr

path to target file.

required
piece_lengthint

piece length for data chunks.

required
+ - - - - + + + + - -
progressbool

default = None

TrueNameTypeDescriptionDefault
-
- Source code in torrentfile\torrent.py -
class FileHasher(CbMixin, ProgMixin):
+    
+    
+        
+          checker
+          
+                Object
+          
+          

the checker instance that maintains variables.

+ + required + + + + hasher + + Object + +

the version specific hashing class for torrent content.

+ + required + + + + + + +
+ Source code in torrentfile\recheck.py +
479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
+641
+642
+643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
class HashChecker(ProgMixin):
     """
-    Calculate root and piece hashes for creating hybrid torrent file.
-
-    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
-    With a branching factor of 2, merge layer hashes until blocks equal
-    piece_length bytes for the piece layer, and then the root hash.
+    Verify that root hashes of content files match the .torrent files.
 
     Parameters
     ----------
-    path : str
-        path to target file.
-    piece_length : int
-        piece length for data chunks.
-    progress : int
-        default = None
+    checker : Object
+        the checker instance that maintains variables.
+    hasher : Object
+        the version specific hashing class for torrent content.
     """
 
-    def __init__(
-        self,
-        path: str,
-        piece_length: int,
-        progress: bool = True,
-        hybrid: bool = False,
-    ):
+    def __init__(self, checker: Checker):
         """
-        Construct Hasher class instances for each file in torrent.
+        Construct a HybridChecker instance.
         """
-        self.path = path
-        self.piece_length = piece_length
-        self.pieces = []
-        self.layer_hashes = []
-        self.piece_layer = None
-        self.root = None
-        self.padding_piece = None
-        self.padding_file = None
-        self.amount = piece_length // BLOCK_SIZE
-        self.end = False
-        self.current = open(path, "rb")
-        self.hybrid = hybrid
-        if progress:
-            self.progressbar = True
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
+        self.checker = checker
+        self.paths = checker.paths
+        self.piece_length = checker.piece_length
+        self.fileinfo = checker.fileinfo
+        self.piece_layers = checker.meta["piece layers"]
+        self.current = None
+        self.index = -1
 
     def __iter__(self):
-        """Return `self`: needed to implement iterator implementation."""
+        """
+        Assign iterator and return self.
+        """
         return self
 
-    def _pad_remaining(self, block_count: int):
+    def __next__(self):
         """
-        Generate Hash sized, 0 filled bytes for padding.
+        Provide the result of comparison.
+        """
+        if self.current is None:
+            self.next_file()
+        try:
+            return self.process_current()
+        except StopIteration as itererr:
+            if self.next_file():
+                return self.process_current()
+            raise StopIteration from itererr
+
+    class Padder:
+        """
+        Padding class to generate padding hashes wherever needed.
 
         Parameters
         ----------
-        block_count : int
-            current total number of blocks collected.
+        length: int
+            the total size of the mock file generating padding for.
+        piece_length : int
+            the block size that each hash represents.
+        """
+
+        def __init__(self, length, piece_length):
+            """
+            Construct padding class to Mock missing or incomplete files.
+
+            Parameters
+            ----------
+            length : int
+                size of the file
+            piece_length : int
+                the piece length for each iteration.
+            """
+            self.length = length
+            self.piece_length = piece_length
+            self.pad = sha256(bytearray(piece_length)).digest()
+
+        def __iter__(self):
+            """
+            Return self to correctly implement iterator type.
+            """
+            return self  # pragma: nocover
+
+        def __next__(self) -> bytes:
+            """
+            Iterate through seemingly endless sha256 hashes of zeros.
+
+            Returns
+            -------
+            tuple :
+                returns the padding
+
+            Raises
+            ------
+            StopIteration
+            """
+            if self.length >= self.piece_length:
+                self.length -= self.piece_length
+                return self.pad
+            if self.length > 0:
+                pad = sha256(bytearray(self.length)).digest()
+                self.length -= self.length
+                return pad
+            raise StopIteration
+
+    def next_file(self) -> bool:
+        """
+        Remove all references to  processed files and prepare for the next.
 
         Returns
         -------
-        padding : bytes
-            Padding to fill remaining portion of tree.
+        bool
+            if there is a next file found
         """
-        # when the there is only one block for file
-        remaining = self.amount - block_count
-        if not self.layer_hashes:
-            power2 = next_power_2(block_count)
-            remaining = power2 - block_count
-        self.prog_update(HASH_SIZE * remaining)
-        return [bytes(HASH_SIZE) for _ in range(remaining)]
+        self.index += 1
+        self.prog_close()
+        if self.current is None or self.index < len(self.paths):
+            self.current = self.paths[self.index]
+            self.length = self.fileinfo[self.index]["length"]
+            self.root_hash = self.fileinfo[self.index]["pieces root"]
+            if self.length > self.piece_length:
+                self.pieces = self.piece_layers[self.root_hash]
+            else:
+                self.pieces = self.root_hash
+            path = self.paths[self.index]
+            self.prog_start(self.length, path, unit="bytes")
+            self.count = 0
+            if os.path.exists(self.current):
+                self.hasher = FileHasher(path, self.piece_length, progress=0)
+            else:
+                self.hasher = self.Padder(self.length, self.piece_length)
+            return True
+        if self.index >= len(self.paths):
+            del self.current
+            del self.length
+            del self.root_hash
+            del self.pieces
+        return False
 
-    def __next__(self):
+    def process_current(self) -> tuple:
         """
-        Calculate layer hashes for contents of file.
+        Gather necessary information to compare to metafile details.
 
-        Parameters
-        ----------
-        data : BytesIO
-            File opened in read mode.
+        Returns
+        -------
+        tuple
+            a tuple containing the layer, piece, current path and size
+
+        Raises
+        ------
+        StopIteration
         """
-        if self.end:
-            self.end = False
-            raise StopIteration
-        plength = self.piece_length
-        blocks = []
-        piece = sha1()  # nosec
-        total = 0
-        block = bytearray(BLOCK_SIZE)
-        for _ in range(self.amount):
-            size = self.current.readinto(block)
+        try:
+            layer = next(self.hasher)
+            piece, size = self.advance()
             self.prog_update(size)
-            if not size:
-                self.end = True
-                break
-            total += size
-            plength -= size
-            blocks.append(sha256(block[:size]).digest())
-            if self.hybrid:
-                piece.update(block[:size])
-        if len(blocks) != self.amount:
-            padding = self._pad_remaining(len(blocks))
-            blocks.extend(padding)
-        layer_hash = merkle_root(blocks)
-        self.layer_hashes.append(layer_hash)
-        if self._cb:
-            self._cb(layer_hash)
-        if self.end:
-            self._calculate_root()
-            self.prog_close()
-        if self.hybrid:
-            if plength > 0:
-                self.padding_file = {
-                    "attr": "p",
-                    "length": plength,
-                    "path": [".pad", str(plength)],
-                }
-                piece.update(bytes(plength))
-            piece = piece.digest()
-            self.pieces.append(piece)
-            return layer_hash, piece
-        return layer_hash
+            return layer, piece, self.current, size
+        except StopIteration as err:
+            if self.length > 0 and self.count * SHA256 < len(self.pieces):
+                self.hasher = self.Padder(self.length, self.piece_length)
+                piece, size = self.advance()
+                layer = next(self.hasher)
+                self.prog_update(0)
+                return layer, piece, self.current, size
+            raise StopIteration from err
 
-    def _calculate_root(self):
+    def advance(self) -> tuple:
         """
-        Calculate the root hash for opened file.
-        """
-        self.piece_layer = b"".join(self.layer_hashes)
-
-        if len(self.layer_hashes) > 1:
-            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
-
-            pow2 = next_power_2(len(self.layer_hashes))
-            remainder = pow2 - len(self.layer_hashes)
+        Increment the number of pieces processed for the current file.
 
-            self.layer_hashes += [pad_piece for _ in range(remainder)]
-        self.root = merkle_root(self.layer_hashes)
-        self.current.close()
+        Returns
+        -------
+        tuple
+            the piece and size
+        """
+        start = self.count * SHA256
+        end = start + SHA256
+        piece = self.pieces[start:end]
+        self.count += 1
+        if self.length >= self.piece_length:
+            self.length -= self.piece_length
+            size = self.piece_length
+        else:
+            size = self.length
+            self.length -= self.length
+        return piece, size
 
- +
+
@@ -10087,177 +14221,269 @@
-
- -
-__init__(self, path, piece_length, progress=True, hybrid=False) - - special - -
-
+
-

Construct Hasher class instances for each file in torrent.

-
- Source code in torrentfile\torrent.py -
def __init__(
-    self,
-    path: str,
-    piece_length: int,
-    progress: bool = True,
-    hybrid: bool = False,
-):
-    """
-    Construct Hasher class instances for each file in torrent.
-    """
-    self.path = path
-    self.piece_length = piece_length
-    self.pieces = []
-    self.layer_hashes = []
-    self.piece_layer = None
-    self.root = None
-    self.padding_piece = None
-    self.padding_file = None
-    self.amount = piece_length // BLOCK_SIZE
-    self.end = False
-    self.current = open(path, "rb")
-    self.hybrid = hybrid
-    if progress:
-        self.progressbar = True
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
-
-
-
-
+
+ Padder -
+
+
-
-__iter__(self) - - special - +

Padding class to generate padding hashes wherever needed.

-
+

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
length +

the total size of the mock file generating padding for.

+ required +
piece_length + int +

the block size that each hash represents.

+ required +
+ + +
+ Source code in torrentfile\recheck.py +
522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
class Padder:
+    """
+    Padding class to generate padding hashes wherever needed.
 
-    
+ Parameters + ---------- + length: int + the total size of the mock file generating padding for. + piece_length : int + the block size that each hash represents. + """ -

Return self: needed to implement iterator implementation.

+ def __init__(self, length, piece_length): + """ + Construct padding class to Mock missing or incomplete files. -
- Source code in torrentfile\torrent.py -
def __iter__(self):
-    """Return `self`: needed to implement iterator implementation."""
-    return self
+        Parameters
+        ----------
+        length : int
+            size of the file
+        piece_length : int
+            the piece length for each iteration.
+        """
+        self.length = length
+        self.piece_length = piece_length
+        self.pad = sha256(bytearray(piece_length)).digest()
+
+    def __iter__(self):
+        """
+        Return self to correctly implement iterator type.
+        """
+        return self  # pragma: nocover
+
+    def __next__(self) -> bytes:
+        """
+        Iterate through seemingly endless sha256 hashes of zeros.
+
+        Returns
+        -------
+        tuple :
+            returns the padding
+
+        Raises
+        ------
+        StopIteration
+        """
+        if self.length >= self.piece_length:
+            self.length -= self.piece_length
+            return self.pad
+        if self.length > 0:
+            pad = sha256(bytearray(self.length)).digest()
+            self.length -= self.length
+            return pad
+        raise StopIteration
 
-
-
+
+
+ + + +
+ -
-
-
-__next__(self) - - special - + + +
+ + + +
+__init__(length, piece_length) +
+
-

Calculate layer hashes for contents of file.

+

Construct padding class to Mock missing or incomplete files.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
dataBytesIO

File opened in read mode.

requiredNameTypeDescriptionDefault
+ + + + length + + int + +

size of the file

+ + required + + + + piece_length + + int + +

the piece length for each iteration.

+ + required + + + + +
- Source code in torrentfile\torrent.py -
def __next__(self):
+          Source code in torrentfile\recheck.py
+          
534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
def __init__(self, length, piece_length):
     """
-    Calculate layer hashes for contents of file.
+    Construct padding class to Mock missing or incomplete files.
 
     Parameters
     ----------
-    data : BytesIO
-        File opened in read mode.
+    length : int
+        size of the file
+    piece_length : int
+        the piece length for each iteration.
     """
-    if self.end:
-        self.end = False
-        raise StopIteration
-    plength = self.piece_length
-    blocks = []
-    piece = sha1()  # nosec
-    total = 0
-    block = bytearray(BLOCK_SIZE)
-    for _ in range(self.amount):
-        size = self.current.readinto(block)
-        self.prog_update(size)
-        if not size:
-            self.end = True
-            break
-        total += size
-        plength -= size
-        blocks.append(sha256(block[:size]).digest())
-        if self.hybrid:
-            piece.update(block[:size])
-    if len(blocks) != self.amount:
-        padding = self._pad_remaining(len(blocks))
-        blocks.extend(padding)
-    layer_hash = merkle_root(blocks)
-    self.layer_hashes.append(layer_hash)
-    if self._cb:
-        self._cb(layer_hash)
-    if self.end:
-        self._calculate_root()
-        self.prog_close()
-    if self.hybrid:
-        if plength > 0:
-            self.padding_file = {
-                "attr": "p",
-                "length": plength,
-                "path": [".pad", str(plength)],
-            }
-            piece.update(bytes(plength))
-        piece = piece.digest()
-        self.pieces.append(piece)
-        return layer_hash, piece
-    return layer_hash
+    self.length = length
+    self.piece_length = piece_length
+    self.pad = sha256(bytearray(piece_length)).digest()
 
+
@@ -10265,52 +14491,34 @@
-
- -
- -
- - - - - -
- +
+__iter__() -
-__init__(self, **kwargs) - - special - +
-
-

Create Bittorrent v1 v2 hybrid metafiles.

+

Return self to correctly implement iterator type.

- Source code in torrentfile\torrent.py -
def __init__(self, **kwargs):
+          Source code in torrentfile\recheck.py
+          
549
+550
+551
+552
+553
def __iter__(self):
     """
-    Create Bittorrent v1 v2 hybrid metafiles.
+    Return self to correctly implement iterator type.
     """
-    super().__init__(**kwargs)
-    logger.debug("Assembling bittorrent Hybrid file")
-    self.name = os.path.basename(self.path)
-    self.hashes = []
-    self.piece_layers = {}
-    self.pieces = bytearray()
-    self.files = []
-    self.hybrid = self.meta_version == "3"
-    self.total = len(utils.get_file_list(self.path))
-    self.assemble()
+    return self  # pragma: nocover
 
+
@@ -10318,43 +14526,102 @@
-
+
-
-assemble(self) +
+__next__() -
+
+
-

Assemble the parts of the torrentfile into meta dictionary.

+

Iterate through seemingly endless sha256 hashes of zeros.

+ +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
tuple + bytes +

returns the padding

+ +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ StopIteration +
- Source code in torrentfile\torrent.py -
def assemble(self):
+          Source code in torrentfile\recheck.py
+          
555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
def __next__(self) -> bytes:
     """
-    Assemble the parts of the torrentfile into meta dictionary.
-    """
-    info = self.meta["info"]
-    info["meta version"] = 2
-
-    if os.path.isfile(self.path):
-        info["file tree"] = {self.name: self._traverse(self.path)}
-        info["length"] = os.path.getsize(self.path)
+    Iterate through seemingly endless sha256 hashes of zeros.
 
-    else:
-        info["file tree"] = self._traverse(self.path)
-        if self.hybrid:
-            info["files"] = self.files
+    Returns
+    -------
+    tuple :
+        returns the padding
 
-    if self.hybrid:
-        info["pieces"] = self.pieces
-    self.meta["piece layers"] = self.piece_layers
-    return info
+    Raises
+    ------
+    StopIteration
+    """
+    if self.length >= self.piece_length:
+        self.length -= self.piece_length
+        return self.pad
+    if self.length > 0:
+        pad = sha256(bytearray(self.length)).digest()
+        self.length -= self.length
+        return pad
+    raise StopIteration
 
+
@@ -10372,321 +14639,131 @@
-
+ +
-

- -TorrentFile (MetaFile, ProgMixin) - +

+__init__(checker) +
-
-

Class for creating Bittorrent meta files.

-

Construct Torrentfile class instance object.

+

Construct a HybridChecker instance.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
kwargsdict

Dictionary containing torrent file options.

{}
- Source code in torrentfile\torrent.py -
class TorrentFile(MetaFile, ProgMixin):
+          Source code in torrentfile\recheck.py
+          
491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
def __init__(self, checker: Checker):
     """
-    Class for creating Bittorrent meta files.
+    Construct a HybridChecker instance.
+    """
+    self.checker = checker
+    self.paths = checker.paths
+    self.piece_length = checker.piece_length
+    self.fileinfo = checker.fileinfo
+    self.piece_layers = checker.meta["piece layers"]
+    self.current = None
+    self.index = -1
+
+
+
+
- Construct *Torrentfile* class instance object. +
- Parameters - ---------- - kwargs : dict - Dictionary containing torrent file options. - """ - hasher = Hasher - def __init__(self, **kwargs): - """ - Construct TorrentFile instance with given keyword args. +
- Parameters - ---------- - kwargs : dict - dictionary of keyword args passed to superclass. - """ - super().__init__(**kwargs) - logger.debug("Assembling bittorrent v1 torrent file") - self.assemble() - def assemble(self): - """ - Assemble components of torrent metafile. - - Returns - ------- - dict - metadata dictionary for torrent file - """ - info = self.meta["info"] - size, filelist = utils.filelist_total(self.path) - if os.path.isfile(self.path): - info["length"] = size - else: - info["files"] = [ - { - "length": os.path.getsize(path), - "path": os.path.relpath(path, self.path).split(os.sep), - } - for path in filelist - ] - pieces = bytearray() - - feeder = Hasher(filelist, self.piece_length, self.progress) - for piece in feeder: - pieces.extend(piece) - - info["pieces"] = pieces -
-
- - - -
- - - - - - - -
- - - -
- -hasher (CbMixin, ProgMixin) - +
+__iter__()
+
-

Piece hasher for Bittorrent V1 files.

-

Takes a sorted list of all file paths, calculates sha1 hash -for fixed size pieces of file data from each file -seemlessly until the last piece which may be smaller than others.

+

Assign iterator and return self.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
pathslist

List of files.

required
piece_lengthint

Size of chuncks to split the data into.

required
progressbool

default = None

True
- Source code in torrentfile\torrent.py -
class Hasher(CbMixin, ProgMixin):
+          Source code in torrentfile\recheck.py
+          
503
+504
+505
+506
+507
def __iter__(self):
     """
-    Piece hasher for Bittorrent V1 files.
-
-    Takes a sorted list of all file paths, calculates sha1 hash
-    for fixed size pieces of file data from each file
-    seemlessly until the last piece which may be smaller than others.
-
-    Parameters
-    ----------
-    paths : list
-        List of files.
-    piece_length : int
-        Size of chuncks to split the data into.
-    progress : int
-        default = None
+    Assign iterator and return self.
     """
-
-    def __init__(self, paths: list, piece_length: int, progress: bool = True):
-        """Generate hashes of piece length data from filelist contents."""
-        self.piece_length = piece_length
-        self.paths = paths
-        self.progress = progress
-        self.total = sum([os.path.getsize(i) for i in self.paths])
-        self.index = 0
-        self.current = open(self.paths[0], "rb")
-        if self.progress:
-            total = os.path.getsize(self.paths[0])
-            self.prog_start(total, self.paths[0], unit="bytes")
-        logger.debug("Hashing %s", str(self.paths[0]))
-
-    def __iter__(self):
-        """
-        Iterate through feed pieces.
-
-        Returns
-        -------
-        self : iterator
-            Iterator for leaves/hash pieces.
-        """
-        return self
-
-    def _handle_partial(self, arr: bytearray) -> bytearray:
-        """
-        Define the handling partial pieces that span 2 or more files.
-
-        Parameters
-        ----------
-        arr : bytearray
-            Incomplete piece containing partial data
-
-        Returns
-        -------
-        digest : bytearray
-            SHA1 digest of the complete piece.
-        """
-        while len(arr) < self.piece_length and self.next_file():
-            target = self.piece_length - len(arr)
-            temp = bytearray(target)
-            size = self.current.readinto(temp)
-            arr.extend(temp[:size])
-            self.prog_update(size)
-            if size == target:
-                break
-        return sha1(arr).digest()  # nosec
-
-    def next_file(self) -> bool:
-        """
-        Seemlessly transition to next file in file list.
-
-        Returns
-        -------
-        bool:
-            True if there is a next file otherwise False.
-        """
-        self.index += 1
-        self.prog_close()
-        if self.index < len(self.paths):
-            path = self.paths[self.index]
-            logger.debug("Hashing %s", str(path))
-            self.current.close()
-            if self.progress:
-                self.prog_start(os.path.getsize(path), path, unit="bytes")
-            self.current = open(path, "rb")
-            return True
-        return False
-
-    def __next__(self) -> bytes:
-        """
-        Generate piece-length pieces of data from input file list.
-
-        Returns
-        -------
-        bytes
-            SHA1 hash of the piece extracted.
-        """
-        while True:
-            piece = bytearray(self.piece_length)
-            size = self.current.readinto(piece)
-            if size == 0:
-                if not self.next_file():
-                    raise StopIteration
-            elif size < self.piece_length:
-                self.prog_update(size)
-                return self._handle_partial(piece[:size])
-            else:
-                self.prog_update(size)
-                return sha1(piece).digest()  # nosec
+    return self
 
+
+
- - -
- - - - - +
+
-
+
+__next__() -
-__init__(self, paths, piece_length, progress=True) - - special - +
-
-

Generate hashes of piece length data from filelist contents.

+

Provide the result of comparison.

- Source code in torrentfile\torrent.py -
def __init__(self, paths: list, piece_length: int, progress: bool = True):
-    """Generate hashes of piece length data from filelist contents."""
-    self.piece_length = piece_length
-    self.paths = paths
-    self.progress = progress
-    self.total = sum([os.path.getsize(i) for i in self.paths])
-    self.index = 0
-    self.current = open(self.paths[0], "rb")
-    if self.progress:
-        total = os.path.getsize(self.paths[0])
-        self.prog_start(total, self.paths[0], unit="bytes")
-    logger.debug("Hashing %s", str(self.paths[0]))
+          Source code in torrentfile\recheck.py
+          
509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
def __next__(self):
+    """
+    Provide the result of comparison.
+    """
+    if self.current is None:
+        self.next_file()
+    try:
+        return self.process_current()
+    except StopIteration as itererr:
+        if self.next_file():
+            return self.process_current()
+        raise StopIteration from itererr
 
+
@@ -10694,51 +14771,82 @@
+
-
-__iter__(self) +
+advance() - - special - -
+
+
-

Iterate through feed pieces.

+

Increment the number of pieces processed for the current file.

+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple +

the piece and size

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
iterator

Iterator for leaves/hash pieces.

- Source code in torrentfile\torrent.py -
def __iter__(self):
+          Source code in torrentfile\recheck.py
+          
638
+639
+640
+641
+642
+643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
def advance(self) -> tuple:
     """
-    Iterate through feed pieces.
+    Increment the number of pieces processed for the current file.
 
     Returns
     -------
-    self : iterator
-        Iterator for leaves/hash pieces.
+    tuple
+        the piece and size
     """
-    return self
+    start = self.count * SHA256
+    end = start + SHA256
+    piece = self.pieces[start:end]
+    self.count += 1
+    if self.length >= self.piece_length:
+        self.length -= self.piece_length
+        size = self.piece_length
+    else:
+        size = self.length
+        self.length -= self.length
+    return piece, size
 
+
@@ -10746,121 +14854,108 @@
+
+ +
+next_file() -
-__next__(self) - - special - +
-
-

Generate piece-length pieces of data from input file list.

+

Remove all references to processed files and prepare for the next.

+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ bool +

if there is a next file found

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytes

SHA1 hash of the piece extracted.

- Source code in torrentfile\torrent.py -
def __next__(self) -> bytes:
+          Source code in torrentfile\recheck.py
+          
577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
def next_file(self) -> bool:
     """
-    Generate piece-length pieces of data from input file list.
+    Remove all references to  processed files and prepare for the next.
 
     Returns
     -------
-    bytes
-        SHA1 hash of the piece extracted.
-    """
-    while True:
-        piece = bytearray(self.piece_length)
-        size = self.current.readinto(piece)
-        if size == 0:
-            if not self.next_file():
-                raise StopIteration
-        elif size < self.piece_length:
-            self.prog_update(size)
-            return self._handle_partial(piece[:size])
-        else:
-            self.prog_update(size)
-            return sha1(piece).digest()  # nosec
-
- - - - - - - -
- - - -
-next_file(self) - - -
- -
- -

Seemlessly transition to next file in file list.

- -

Returns:

- - - - - - - - - - - - - -
TypeDescription
bool

True if there is a next file otherwise False.

-
- Source code in torrentfile\torrent.py -
def next_file(self) -> bool:
-    """
-    Seemlessly transition to next file in file list.
-
-    Returns
-    -------
-    bool:
-        True if there is a next file otherwise False.
+    bool
+        if there is a next file found
     """
     self.index += 1
     self.prog_close()
-    if self.index < len(self.paths):
+    if self.current is None or self.index < len(self.paths):
+        self.current = self.paths[self.index]
+        self.length = self.fileinfo[self.index]["length"]
+        self.root_hash = self.fileinfo[self.index]["pieces root"]
+        if self.length > self.piece_length:
+            self.pieces = self.piece_layers[self.root_hash]
+        else:
+            self.pieces = self.root_hash
         path = self.paths[self.index]
-        logger.debug("Hashing %s", str(path))
-        self.current.close()
-        if self.progress:
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
-        self.current = open(path, "rb")
+        self.prog_start(self.length, path, unit="bytes")
+        self.count = 0
+        if os.path.exists(self.current):
+            self.hasher = FileHasher(path, self.piece_length, progress=0)
+        else:
+            self.hasher = self.Padder(self.length, self.piece_length)
         return True
+    if self.index >= len(self.paths):
+        del self.current
+        del self.length
+        del self.root_hash
+        del self.pieces
     return False
 
+
@@ -10868,136 +14963,112 @@
+
-
-__init__(self, **kwargs) +
+process_current() - - special -
+
-

Construct TorrentFile instance with given keyword args.

+

Gather necessary information to compare to metafile details.

-

Parameters:

- - - - - - - - - - +

Returns:

+
NameTypeDescriptionDefault
+ - - - - + + - -
kwargsdict

dictionary of keyword args passed to superclass.

{}TypeDescription
-
- Source code in torrentfile\torrent.py -
def __init__(self, **kwargs):
-    """
-    Construct TorrentFile instance with given keyword args.
-
-    Parameters
-    ----------
-    kwargs : dict
-        dictionary of keyword args passed to superclass.
-    """
-    super().__init__(**kwargs)
-    logger.debug("Assembling bittorrent v1 torrent file")
-    self.assemble()
-
-
-
- -
- - - -
- - - -
-assemble(self) - - -
- -
- -

Assemble components of torrent metafile.

+ + + + + tuple + +

a tuple containing the layer, piece, current path and size

+ + + + +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ StopIteration +
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
dict

metadata dictionary for torrent file

- Source code in torrentfile\torrent.py -
def assemble(self):
+          Source code in torrentfile\recheck.py
+          
611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
def process_current(self) -> tuple:
     """
-    Assemble components of torrent metafile.
+    Gather necessary information to compare to metafile details.
 
     Returns
     -------
-    dict
-        metadata dictionary for torrent file
-    """
-    info = self.meta["info"]
-    size, filelist = utils.filelist_total(self.path)
-    if os.path.isfile(self.path):
-        info["length"] = size
-    else:
-        info["files"] = [
-            {
-                "length": os.path.getsize(path),
-                "path": os.path.relpath(path, self.path).split(os.sep),
-            }
-            for path in filelist
-        ]
-    pieces = bytearray()
-
-    feeder = Hasher(filelist, self.piece_length, self.progress)
-    for piece in feeder:
-        pieces.extend(piece)
+    tuple
+        a tuple containing the layer, piece, current path and size
 
-    info["pieces"] = pieces
+    Raises
+    ------
+    StopIteration
+    """
+    try:
+        layer = next(self.hasher)
+        piece, size = self.advance()
+        self.prog_update(size)
+        return layer, piece, self.current, size
+    except StopIteration as err:
+        if self.length > 0 and self.count * SHA256 < len(self.pieces):
+            self.hasher = self.Padder(self.length, self.piece_length)
+            piece, size = self.advance()
+            layer = next(self.hasher)
+            self.prog_update(0)
+            return layer, piece, self.current, size
+        raise StopIteration from err
 
+
@@ -11015,140 +15086,227 @@
-
- - - -

- -TorrentFileHybrid (MetaFile, ProgMixin) - - - - -

- -
- -

Construct the Hybrid torrent meta file with provided parameters.

-

DEPRECATED

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
kwargsdict

Keyword arguments for torrent options.

{}
-
- Source code in torrentfile\torrent.py -
class TorrentFileHybrid(MetaFile, ProgMixin):
-    """
-    Construct the Hybrid torrent meta file with provided parameters.
-
-    **DEPRECATED**
-
-    Parameters
-    ----------
-    kwargs : dict
-        Keyword arguments for torrent options.
-    """
 
-    hasher = HasherHybrid
 
-    def __init__(self, **kwargs):
-        """
-        Create Bittorrent v1 v2 hybrid metafiles.
-        """
-        super().__init__(**kwargs)
-        logger.debug("Assembling bittorrent Hybrid file")
-        self.name = os.path.basename(self.path)
-        self.hashes = []
-        self.piece_layers = {}
-        self.pieces = []
-        self.files = []
-        self.total = len(utils.get_file_list(self.path))
-        self.assemble()
 
-    def assemble(self):
-        """
-        Assemble the parts of the torrentfile into meta dictionary.
+  
- **DEPRECATED** - """ - info = self.meta["info"] - info["meta version"] = 2 +
- if os.path.isfile(self.path): - info["file tree"] = {self.name: self._traverse(self.path)} - info["length"] = os.path.getsize(self.path) +
- else: - info["file tree"] = self._traverse(self.path) - info["files"] = self.files - info["pieces"] = b"".join(self.pieces) - self.meta["piece layers"] = self.piece_layers - return info - def _traverse(self, path: str) -> dict: - """ - Build meta dictionary while walking directory. +
- **DEPRECATED** - Parameters - ---------- - path : str - Path to target file. - """ - if os.path.isfile(path): - file_size = os.path.getsize(path) - self.files.append( - { - "length": file_size, - "path": os.path.relpath(path, self.path).split(os.sep), - } - ) +

+ torrent - if file_size == 0: - return {"": {"length": file_size}} - logger.debug("Hashing %s", str(path)) - file_hash = HasherHybrid(path, self.piece_length, self.progress) - self.prog_update(file_size) - if file_size > self.piece_length: - self.piece_layers[file_hash.root] = file_hash.piece_layer +

- self.hashes.append(file_hash) - self.pieces.extend(file_hash.pieces) +
- if file_hash.padding_file: - self.files.append(file_hash.padding_file) - - return {"": {"length": file_size, "pieces root": file_hash.root}} - - tree = {} - if os.path.isdir(path): - for name in sorted(os.listdir(path)): - tree[name] = self._traverse(os.path.join(path, name)) - return tree -
- +

Classes and procedures pertaining to the creation of torrent meta files.

+
Classes
+
    +
  • +

    TorrentFile + construct .torrent file.

    +
  • +
  • +

    TorrentFileV2 + construct .torrent v2 files using provided data.

    +
  • +
  • +

    MetaFile + base class for all MetaFile classes.

    +
  • +
+
Constants
+
    +
  • +

    BLOCK_SIZE : int + size of leaf hashes for merkle tree.

    +
  • +
  • +

    HASH_SIZE : int + Length of a sha256 hash.

    +
  • +
+
Bittorrent V2
+

From Bittorrent.org Documentation pages.

+

Implementation details for Bittorrent Protocol v2.

+
+

Note

+

All strings in a .torrent file that contain text must be UTF-8 encoded.

+
+
Meta Version 2 Dictionary:
+
    +
  • +

    "announce": + The URL of the tracker.

    +
  • +
  • +

    "info": + This maps to a dictionary, with keys described below.

    +
      +
    • +

      "name": + A display name for the torrent. It is purely advisory.

      +
    • +
    • +

      "piece length": + The number of bytes that each logical piece in the peer + protocol refers to. I.e. it sets the granularity of piece, request, + bitfield and have messages. It must be a power of two and at least + 6KiB.

      +
    • +
    • +

      "meta version": + An integer value, set to 2 to indicate compatibility + with the current revision of this specification. Version 1 is not + assigned to avoid confusion with BEP3. Future revisions will only + increment this issue to indicate an incompatible change has been made, + for example that hash algorithms were changed due to newly discovered + vulnerabilities. Lementations must check this field first and indicate + that a torrent is of a newer version than they can handle before + performing other idations which may result in more general messages + about invalid files. Files are mapped into this piece address space so + that each non-empty

      +
    • +
    • +

      "file tree": + A tree of dictionaries where dictionary keys represent UTF-8 + encoded path elements. Entries with zero-length keys describe the + properties of the composed path at that point. 'UTF-8 encoded' + context only means that if the native encoding is known at creation + time it must be converted to UTF-8. Keys may contain invalid UTF-8 + sequences or characters and names that are reserved on specific + filesystems. Implementations must be prepared to sanitize them. On + platforms path components exactly matching '.' and '..' must be + sanitized since they could lead to directory traversal attacks and + conflicting path descriptions. On platforms that require UTF-8 + path components this sanitizing step must happen after normalizing + overlong UTF-8 encodings. + File is aligned to a piece boundary and occurs in same order as + the file tree. The last piece of each file may be shorter than the + specified piece length, resulting in an alignment gap.

      +
    • +
    • +

      "length": + Length of the file in bytes. Presence of this field indicates + that the dictionary describes a file, not a directory. Which means + it must not have any sibling entries.

      +
    • +
    • +

      "pieces root": + For non-empty files this is the the root hash of a merkle + tree with a branching factor of 2, constructed from 16KiB blocks + of the file. The last block may be shorter than 16KiB. The + remaining leaf hashes beyond the end of the file required to + construct upper layers of the merkle tree are set to zero. As of + meta version 2 SHA2-256 is used as digest function for the merkle + tree. The hash is stored in its binary form, not as human-readable + string.

      +
    • +
    +
  • +
  • +

    "piece layers": + A dictionary of strings. For each file in the file tree that + is larger than the piece size it contains one string value. + The keys are the merkle roots while the values consist of concatenated + hashes of one layer within that merkle tree. The layer is chosen so + that one hash covers piece length bytes. For example if the piece + size is 16KiB then the leaf hashes are used. If a piece size of + 128KiB is used then 3rd layer up from the leaf hashes is used. Layer + hashes which exclusively cover data beyond the end of file, i.e. + are only needed to balance the tree, are omitted. All hashes are + stored in their binary format. A torrent is not valid if this field is + absent, the contained hashes do not match the merkle roots or are + not from the correct layer.

    +
  • +
+
+

Important

+

The file tree root dictionary itself must not be a file, +i.e. it must not contain a zero-length key with a dictionary containing +a length key.

+
+
Bittorrent V1
+
v1 meta-dictionary
+
    +
  • +

    announce: + The URL of the tracker.

    +
  • +
  • +

    info: + This maps to a dictionary, with keys described below.

    +
      +
    • +

      name: + maps to a UTF-8 encoded string which is the suggested name to + save the file (or directory) as. It is purely advisory.

      +
    • +
    • +

      piece length: + maps to the number of bytes in each piece the file is split + into. For the purposes of transfer, files are split into + fixed-size pieces which are all the same length except for + possibly the last one which may be truncated.

      +
    • +
    • +

      piece length: + is almost always a power of two, most commonly 2^18 = 256 K

      +
    • +
    • +

      pieces: + maps to a string whose length is a multiple of 20. It is to be + subdivided into strings of length 20, each of which is the SHA1 + hash of the piece at the corresponding index.

      +
    • +
    • +

      length: + In the single file case, maps to the length of the file in bytes.

      +
    • +
    • +

      files: + If present then the download represents a single file, otherwise it + represents a set of files which go in a directory structure. + For the purposes of the other keys, the multi-file case is treated + as only having a single file by concatenating the files in the order + they appear in the files list. The files list is the value files + maps to, and is a list of dictionaries containing the following keys:

      +
        +
      • +

        path: + A list of UTF-8 encoded strings corresponding to subdirectory + names, the last of which is the actual file name

        +
      • +
      • +

        length: + Maps to the length of the file in bytes.

        +
      • +
      +
    • +
    • +

      length: + Only present if the content is a single file. Maps to the length + of the file in bytes.

      +
    • +
    +
  • +
+
+

Note

+

In the single file case, the name key is the name of a file, +in the muliple file case, it's the name of a directory.

+
@@ -11160,188 +15318,571 @@

+
-
- -hasher (CbMixin, ProgMixin) - +

+ MetaFile -

+

+
-

Calculate root and piece hashes for creating hybrid torrent file.

-

DEPRECATED

-

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. -With a branching factor of 2, merge layer hashes until blocks equal -piece_length bytes for the piece layer, and then the root hash.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - +

Base Class for all TorrentFile classes.

+ +

Parameters:

+
NameTypeDescriptionDefault
pathstr

path to target file.

required
piece_lengthint

piece length for data chunks.

required
+ - - - - + + + + - -
progressbool

default = None

TrueNameTypeDescriptionDefault
-
- Source code in torrentfile\torrent.py -
class HasherHybrid(CbMixin, ProgMixin):
+    
+    
+        
+          path
+          
+                str
+          
+          

target path to torrent content. Default: None

+ + None + + + + announce + + str + +

One or more tracker URL's. Default: None

+ + None + + + + comment + + str + +

A comment. Default: None

+ + None + + + + piece_length + + int + +

Size of torrent pieces. Default: None

+ + None + + + + private + + bool + +

For private trackers. Default: None

+ + False + + + + outfile + + str + +

target path to write .torrent file. Default: None

+ + None + + + + source + + str + +

Private tracker source. Default: None

+ + None + + + + progress + + str + +

level of progress bar displayed Default: "1"

+ + 1 + + + + cwd + + bool + +

If True change default save location to current directory

+ + False + + + + httpseeds + + list + +

one or more web addresses where torrent content can be found.

+ + None + + + + url_list + + list + +

one or more web addressess where torrent content exists.

+ + None + + + + content + + str + +

alias for 'path' arg.

+ + None + + + + meta_version + + int + +

indicates which Bittorrent protocol to use for hashing content

+ + None + + + + + + +
+ Source code in torrentfile\torrent.py +
204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
class MetaFile:
     """
-    Calculate root and piece hashes for creating hybrid torrent file.
-
-    **DEPRECATED**
-
-    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
-    With a branching factor of 2, merge layer hashes until blocks equal
-    piece_length bytes for the piece layer, and then the root hash.
+    Base Class for all TorrentFile classes.
 
     Parameters
     ----------
     path : str
-        path to target file.
+        target path to torrent content.  Default: None
+    announce : str
+        One or more tracker URL's.  Default: None
+    comment : str
+        A comment.  Default: None
     piece_length : int
-        piece length for data chunks.
-    progress : int
-        default = None
-    """
-
-    def __init__(self, path: str, piece_length: int, progress: bool = True):
-        """
-        Construct Hasher class instances for each file in torrent.
-
-        **DEPRECATED**
-        """
-        self.path = path
-        self.piece_length = piece_length
-        self.pieces = []
-        self.layer_hashes = []
-        self.piece_layer = None
-        self.root = None
-        self.padding_piece = None
-        self.padding_file = None
-        if progress:
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
-        self.amount = piece_length // BLOCK_SIZE
-        with open(path, "rb") as data:
-            self.process_file(data)
+        Size of torrent pieces.  Default: None
+    private : bool
+        For private trackers.  Default: None
+    outfile : str
+        target path to write .torrent file. Default: None
+    source : str
+        Private tracker source. Default: None
+    progress : str
+        level of progress bar displayed  Default: "1"
+    cwd : bool
+        If True change default save location to current directory
+    httpseeds : list
+        one or more web addresses where torrent content can be found.
+    url_list : list
+        one or more web addressess where torrent content exists.
+    content : str
+        alias for 'path' arg.
+    meta_version : int
+        indicates which Bittorrent protocol to use for hashing content
+    """
 
-    def _pad_remaining(self, block_count: int):
-        """
-        Generate Hash sized, 0 filled bytes for padding.
+    hasher = None
 
-        **DEPRECATED**
+    @classmethod
+    def set_callback(cls, func):
+        """
+        Assign a callback function for the Hashing class to call for each hash.
 
         Parameters
         ----------
-        block_count : int
-            current total number of blocks collected.
-
-        Returns
-        -------
-        padding : bytes
-            Padding to fill remaining portion of tree.
+        func : function
+            The callback function which accepts a single paramter.
         """
-        # when the there is only one block for file
-        remaining = self.amount - block_count
-        if not self.layer_hashes:
-            power2 = next_power_2(block_count)
-            remaining = power2 - block_count
-        self.prog_update(HASH_SIZE * remaining)
-        return [bytes(HASH_SIZE) for _ in range(remaining)]
+        if "hasher" in vars(cls) and vars(cls)["hasher"]:
+            cls.hasher.set_callback(func)
 
-    def process_file(self, data: bytearray):
+    def __init__(
+        self,
+        path=None,
+        announce=None,
+        comment=None,
+        piece_length=None,
+        private=False,
+        outfile=None,
+        source=None,
+        progress=1,
+        cwd=False,
+        httpseeds=None,
+        url_list=None,
+        content=None,
+        meta_version=None,
+        **_,
+    ):
         """
-        Calculate layer hashes for contents of file.
+        Construct MetaFile superclass and assign local attributes.
+        """
+        self.private = private
+        self.cwd = cwd
+        self.outfile = outfile
+        self.progress = int(progress)
+        self.comment = comment
+        self.source = source
+        self.meta_version = meta_version
 
-        **DEPRECATED**
+        if content:
+            path = content
+        if not path:
+            if announce and len(announce) > 1 and os.path.exists(announce[-1]):
+                path = announce[-1]
+                announce = announce[:-1]
+            elif url_list and os.path.exists(url_list[-1]):
+                path = url_list[-1]
+                url_list = url_list[:-1]
+            elif httpseeds and os.path.exists(httpseeds[-1]):
+                path = httpseeds[-1]
+                httpseeds = httpseeds[:-1]
+            else:
+                raise utils.MissingPathError("Path to content is required.")
 
-        Parameters
-        ----------
-        data : BytesIO
-            File opened in read mode.
-        """
-        while True:
-            plength = self.piece_length
-            blocks = []
-            piece = sha1()  # nosec
-            total = 0
-            block = bytearray(BLOCK_SIZE)
-            for _ in range(self.amount):
-                size = data.readinto(block)
-                self.prog_update(size)
-                if not size:
-                    break
-                total += size
-                plength -= size
-                blocks.append(sha256(block[:size]).digest())
-                piece.update(block[:size])
-            if not blocks:
-                break
-            if len(blocks) != self.amount:
-                padding = self._pad_remaining(len(blocks))
-                blocks.extend(padding)
-            layer_hash = merkle_root(blocks)
-            if self._cb:
-                self._cb(layer_hash)
-            self.layer_hashes.append(layer_hash)
-            if plength > 0:
-                self.padding_file = {
-                    "attr": "p",
-                    "length": plength,
-                    "path": [".pad", str(plength)],
-                }
-                piece.update(bytes(plength))
-            self.pieces.append(piece.digest())  # nosec
-        self._calculate_root()
-        self.prog_close()
+        # base path to torrent content.
+        self.path = path
 
-    def _calculate_root(self):
+        logger.debug("path parameter found %s", path)
+
+        # Format piece_length attribute.
+        if piece_length:
+            self.piece_length = utils.normalize_piece_length(piece_length)
+            logger.debug("piece length parameter found %s", piece_length)
+        else:
+            self.piece_length = utils.path_piece_length(self.path)
+            logger.debug("piece length calculated %s", self.piece_length)
+
+        # Assign announce URL to empty string if none provided.
+        if not announce:
+            self.announce, self.announce_list = "", [[""]]
+
+        # Most torrent clients have editting trackers as a feature.
+        elif isinstance(announce, str):
+            self.announce, self.announce_list = announce, [[announce]]
+
+        elif isinstance(announce, Sequence):
+            self.announce, self.announce_list = announce[0], [announce]
+
+        self.meta = {
+            "announce": self.announce,
+            "announce-list": self.announce_list,
+            "created by": f"TorrentFile:v{version}",
+            "creation date": int(datetime.timestamp(datetime.now())),
+            "info": {},
+        }
+        if comment:
+            self.meta["info"]["comment"] = comment
+            logger.debug("comment parameter found %s", comment)
+        if private:
+            self.meta["info"]["private"] = 1
+            logger.debug("private parameter triggered")
+        if source:
+            self.meta["info"]["source"] = source
+            logger.debug("source parameter found %s", source)
+        if url_list:
+            self.meta["url-list"] = url_list
+            logger.debug("url list parameter found %s", str(url_list))
+        if httpseeds:
+            self.meta["httpseeds"] = httpseeds
+            logger.debug("httpseeds parameter found %s", str(httpseeds))
+        self.meta["info"]["piece length"] = self.piece_length
+
+        self.meta_version = meta_version
+        parent, self.name = os.path.split(self.path)
+        if not self.name:
+            self.name = os.path.basename(parent)
+        self.meta["info"]["name"] = self.name
+
+    def assemble(self):
         """
-        Calculate the root hash for opened file.
+        Overload in subclasses.
 
-        **DEPRECATED**
+        Raises
+        ------
+        Exception
+            NotImplementedError
         """
-        self.piece_layer = b"".join(self.layer_hashes)
+        raise NotImplementedError
 
-        if len(self.layer_hashes) > 1:
-            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
+    def sort_meta(self):
+        """Sort the info and meta dictionaries."""
+        logger.debug("sorting dictionary keys")
+        meta = self.meta
+        meta["info"] = dict(sorted(list(meta["info"].items())))
+        meta = dict(sorted(list(meta.items())))
+        return meta
 
-            pow2 = next_power_2(len(self.layer_hashes))
-            remainder = pow2 - len(self.layer_hashes)
+    def write(self, outfile=None) -> tuple:
+        """
+        Write meta information to .torrent file.
 
-            self.layer_hashes += [pad_piece for _ in range(remainder)]
-        self.root = merkle_root(self.layer_hashes)
+        Parameters
+        ----------
+        outfile : str
+            Destination path for .torrent file. default=None
+
+        Returns
+        -------
+        outfile : str
+            Where the .torrent file was writen.
+        meta : dict
+            .torrent meta information.
+        """
+        fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
+        if not self.outfile and not outfile:
+            if self.cwd:
+                self.outfile = fallback
+            else:
+                path = str(self.path).rstrip("\\/")
+                self.outfile = path + ".torrent"
+        elif outfile:
+            self.outfile = outfile
+        if str(self.outfile)[-1] in "\\/":
+            self.outfile = self.outfile + (self.name + ".torrent")
+        self.meta = self.sort_meta()
+        try:
+            pyben.dump(self.meta, self.outfile)
+        except PermissionError:
+            self.outfile = fallback
+            pyben.dump(self.meta, fallback)
+        return self.outfile, self.meta
 
- +
+
@@ -11355,187 +15896,290 @@
-
-
-__init__(self, path, piece_length, progress=True) - - special - -
-
-

Construct Hasher class instances for each file in torrent.

-

DEPRECATED

-
- Source code in torrentfile\torrent.py -
def __init__(self, path: str, piece_length: int, progress: bool = True):
-    """
-    Construct Hasher class instances for each file in torrent.
-
-    **DEPRECATED**
-    """
-    self.path = path
-    self.piece_length = piece_length
-    self.pieces = []
-    self.layer_hashes = []
-    self.piece_layer = None
-    self.root = None
-    self.padding_piece = None
-    self.padding_file = None
-    if progress:
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
-    self.amount = piece_length // BLOCK_SIZE
-    with open(path, "rb") as data:
-        self.process_file(data)
-
-
-
-
+
-
+
+__init__(path=None, announce=None, comment=None, piece_length=None, private=False, outfile=None, source=None, progress=1, cwd=False, httpseeds=None, url_list=None, content=None, meta_version=None, **_) -
-process_file(self, data) +
-
-

Calculate layer hashes for contents of file.

-

DEPRECATED

+

Construct MetaFile superclass and assign local attributes.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
databytearray

File opened in read mode.

required
Source code in torrentfile\torrent.py -
def process_file(self, data: bytearray):
+          
253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
def __init__(
+    self,
+    path=None,
+    announce=None,
+    comment=None,
+    piece_length=None,
+    private=False,
+    outfile=None,
+    source=None,
+    progress=1,
+    cwd=False,
+    httpseeds=None,
+    url_list=None,
+    content=None,
+    meta_version=None,
+    **_,
+):
     """
-    Calculate layer hashes for contents of file.
+    Construct MetaFile superclass and assign local attributes.
+    """
+    self.private = private
+    self.cwd = cwd
+    self.outfile = outfile
+    self.progress = int(progress)
+    self.comment = comment
+    self.source = source
+    self.meta_version = meta_version
 
-    **DEPRECATED**
+    if content:
+        path = content
+    if not path:
+        if announce and len(announce) > 1 and os.path.exists(announce[-1]):
+            path = announce[-1]
+            announce = announce[:-1]
+        elif url_list and os.path.exists(url_list[-1]):
+            path = url_list[-1]
+            url_list = url_list[:-1]
+        elif httpseeds and os.path.exists(httpseeds[-1]):
+            path = httpseeds[-1]
+            httpseeds = httpseeds[:-1]
+        else:
+            raise utils.MissingPathError("Path to content is required.")
 
-    Parameters
-    ----------
-    data : BytesIO
-        File opened in read mode.
-    """
-    while True:
-        plength = self.piece_length
-        blocks = []
-        piece = sha1()  # nosec
-        total = 0
-        block = bytearray(BLOCK_SIZE)
-        for _ in range(self.amount):
-            size = data.readinto(block)
-            self.prog_update(size)
-            if not size:
-                break
-            total += size
-            plength -= size
-            blocks.append(sha256(block[:size]).digest())
-            piece.update(block[:size])
-        if not blocks:
-            break
-        if len(blocks) != self.amount:
-            padding = self._pad_remaining(len(blocks))
-            blocks.extend(padding)
-        layer_hash = merkle_root(blocks)
-        if self._cb:
-            self._cb(layer_hash)
-        self.layer_hashes.append(layer_hash)
-        if plength > 0:
-            self.padding_file = {
-                "attr": "p",
-                "length": plength,
-                "path": [".pad", str(plength)],
-            }
-            piece.update(bytes(plength))
-        self.pieces.append(piece.digest())  # nosec
-    self._calculate_root()
-    self.prog_close()
-
- - + # base path to torrent content. + self.path = path - + logger.debug("path parameter found %s", path) + # Format piece_length attribute. + if piece_length: + self.piece_length = utils.normalize_piece_length(piece_length) + logger.debug("piece length parameter found %s", piece_length) + else: + self.piece_length = utils.path_piece_length(self.path) + logger.debug("piece length calculated %s", self.piece_length) + # Assign announce URL to empty string if none provided. + if not announce: + self.announce, self.announce_list = "", [[""]] + # Most torrent clients have editting trackers as a feature. + elif isinstance(announce, str): + self.announce, self.announce_list = announce, [[announce]] + elif isinstance(announce, Sequence): + self.announce, self.announce_list = announce[0], [announce] - + self.meta = { + "announce": self.announce, + "announce-list": self.announce_list, + "created by": f"TorrentFile:v{version}", + "creation date": int(datetime.timestamp(datetime.now())), + "info": {}, + } + if comment: + self.meta["info"]["comment"] = comment + logger.debug("comment parameter found %s", comment) + if private: + self.meta["info"]["private"] = 1 + logger.debug("private parameter triggered") + if source: + self.meta["info"]["source"] = source + logger.debug("source parameter found %s", source) + if url_list: + self.meta["url-list"] = url_list + logger.debug("url list parameter found %s", str(url_list)) + if httpseeds: + self.meta["httpseeds"] = httpseeds + logger.debug("httpseeds parameter found %s", str(httpseeds)) + self.meta["info"]["piece length"] = self.piece_length + self.meta_version = meta_version + parent, self.name = os.path.split(self.path) + if not self.name: + self.name = os.path.basename(parent) + self.meta["info"]["name"] = self.name + +
+
- - -
+
-
-__init__(self, **kwargs) +
+assemble() - - special -
+
-

Create Bittorrent v1 v2 hybrid metafiles.

+

Overload in subclasses.

+ +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ Exception +

NotImplementedError

Source code in torrentfile\torrent.py -
def __init__(self, **kwargs):
+          
350
+351
+352
+353
+354
+355
+356
+357
+358
+359
def assemble(self):
     """
-    Create Bittorrent v1 v2 hybrid metafiles.
+    Overload in subclasses.
+
+    Raises
+    ------
+    Exception
+        NotImplementedError
     """
-    super().__init__(**kwargs)
-    logger.debug("Assembling bittorrent Hybrid file")
-    self.name = os.path.basename(self.path)
-    self.hashes = []
-    self.piece_layers = {}
-    self.pieces = []
-    self.files = []
-    self.total = len(utils.get_file_list(self.path))
-    self.assemble()
+    raise NotImplementedError
 
+
@@ -11543,44 +16187,75 @@
-
+
-
-assemble(self) +
+set_callback(func) + + classmethod +
+
-

Assemble the parts of the torrentfile into meta dictionary.

-

DEPRECATED

+

Assign a callback function for the Hashing class to call for each hash.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
func + function +

The callback function which accepts a single paramter.

+ required +
Source code in torrentfile\torrent.py -
def assemble(self):
+          
240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
@classmethod
+def set_callback(cls, func):
     """
-    Assemble the parts of the torrentfile into meta dictionary.
+    Assign a callback function for the Hashing class to call for each hash.
 
-    **DEPRECATED**
+    Parameters
+    ----------
+    func : function
+        The callback function which accepts a single paramter.
     """
-    info = self.meta["info"]
-    info["meta version"] = 2
-
-    if os.path.isfile(self.path):
-        info["file tree"] = {self.name: self._traverse(self.path)}
-        info["length"] = os.path.getsize(self.path)
-
-    else:
-        info["file tree"] = self._traverse(self.path)
-        info["files"] = self.files
-
-    info["pieces"] = b"".join(self.pieces)
-    self.meta["piece layers"] = self.piece_layers
-    return info
+    if "hasher" in vars(cls) and vars(cls)["hasher"]:
+        cls.hasher.set_callback(func)
 
+
@@ -11588,141 +16263,440 @@
+
-
-
+
+sort_meta() -
+
-
+
+

Sort the info and meta dictionaries.

+
+ Source code in torrentfile\torrent.py +
361
+362
+363
+364
+365
+366
+367
def sort_meta(self):
+    """Sort the info and meta dictionaries."""
+    logger.debug("sorting dictionary keys")
+    meta = self.meta
+    meta["info"] = dict(sorted(list(meta["info"].items())))
+    meta = dict(sorted(list(meta.items())))
+    return meta
+
+
+
+
-

- -TorrentFileV2 (MetaFile, ProgMixin) - +

-
+
-
-

Class for creating Bittorrent meta v2 files.

-

DEPRECATED

-

Parameters:

- - - - - - - - - - +
+write(outfile=None) + + +
+ + +
+ +

Write meta information to .torrent file.

+ +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
outfile + str +

Destination path for .torrent file. default=None

+ None +
+ +

Returns:

+ + - - - - + + - -
kwargsdict

Keyword arguments for torrent file options.

{}Name TypeDescription
+ + + +outfile + str + +

Where the .torrent file was writen.

+ + +meta + dict + +

.torrent meta information.

+ + + +
Source code in torrentfile\torrent.py -
class TorrentFileV2(MetaFile, ProgMixin):
+          
369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
def write(self, outfile=None) -> tuple:
     """
-    Class for creating Bittorrent meta v2 files.
+    Write meta information to .torrent file.
+
+    Parameters
+    ----------
+    outfile : str
+        Destination path for .torrent file. default=None
+
+    Returns
+    -------
+    outfile : str
+        Where the .torrent file was writen.
+    meta : dict
+        .torrent meta information.
+    """
+    fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
+    if not self.outfile and not outfile:
+        if self.cwd:
+            self.outfile = fallback
+        else:
+            path = str(self.path).rstrip("\\/")
+            self.outfile = path + ".torrent"
+    elif outfile:
+        self.outfile = outfile
+    if str(self.outfile)[-1] in "\\/":
+        self.outfile = self.outfile + (self.name + ".torrent")
+    self.meta = self.sort_meta()
+    try:
+        pyben.dump(self.meta, self.outfile)
+    except PermissionError:
+        self.outfile = fallback
+        pyben.dump(self.meta, fallback)
+    return self.outfile, self.meta
+
+
+
+
+ +
+ + + + + +
+ +
+ +
+ - **DEPRECATED** + +
+ + + +

+ TorrentAssembler + + + +

+ + +
+

+ Bases: MetaFile

+ + +

Assembler class for Bittorrent version 2 and hybrid meta files.

+

This differs from the TorrentFileV2 and TorrentFileHybrid, because +it can be used as an iterator and works for both versions.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
kwargs + dict +

Keyword arguments for torrent options.

+ required +
+ + +
+ Source code in torrentfile\torrent.py +
643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
+658
+659
+660
+661
+662
+663
+664
+665
+666
+667
+668
+669
+670
+671
+672
+673
+674
+675
+676
+677
+678
+679
+680
+681
+682
+683
+684
+685
+686
+687
+688
+689
+690
+691
+692
+693
+694
+695
+696
+697
+698
+699
+700
+701
+702
+703
+704
+705
+706
+707
+708
+709
+710
+711
+712
+713
+714
+715
+716
+717
+718
+719
+720
+721
+722
+723
+724
+725
+726
+727
+728
+729
+730
+731
+732
+733
+734
+735
+736
+737
+738
+739
class TorrentAssembler(MetaFile):
+    """
+    Assembler class for Bittorrent version 2 and hybrid meta files.
+
+    This differs from the TorrentFileV2 and TorrentFileHybrid, because
+    it can be used as an iterator and works for both versions.
 
     Parameters
     ----------
     kwargs : dict
-        Keyword arguments for torrent file options.
+        Keyword arguments for torrent options.
     """
 
-    hasher = HasherV2
+    hasher = FileHasher
 
     def __init__(self, **kwargs):
         """
-        Construct `TorrentFileV2` Class instance from given parameters.
-
-        **DEPRECATED**
-
-        Parameters
-        ----------
-        kwargs : dict
-            keywword arguments to pass to superclass.
+        Create Bittorrent v1 v2 hybrid metafiles.
         """
         super().__init__(**kwargs)
-        logger.debug("Assembling bittorrent v2 torrent file")
-        self.piece_layers = {}
+        logger.debug("Assembling bittorrent Hybrid file")
+        self.name = os.path.basename(self.path)
         self.hashes = []
+        self.piece_layers = {}
+        self.pieces = bytearray()
+        self.files = []
+        self.hybrid = self.meta_version == "3"
         self.total = len(utils.get_file_list(self.path))
         self.assemble()
 
     def assemble(self):
         """
-        Assemble then return the meta dictionary for encoding.
-
-        **DEPRECATED**
-
-        Returns
-        -------
-        meta : dict
-            Metainformation about the torrent.
+        Assemble the parts of the torrentfile into meta dictionary.
         """
         info = self.meta["info"]
+        info["meta version"] = 2
+
         if os.path.isfile(self.path):
-            info["file tree"] = {info["name"]: self._traverse(self.path)}
+            info["file tree"] = {self.name: self._traverse(self.path)}
             info["length"] = os.path.getsize(self.path)
-            self.prog_update(info["length"])
+
         else:
             info["file tree"] = self._traverse(self.path)
+            if self.hybrid:
+                info["files"] = self.files
 
-        info["meta version"] = 2
+        if self.hybrid:
+            info["pieces"] = self.pieces
         self.meta["piece layers"] = self.piece_layers
+        return info
 
     def _traverse(self, path: str) -> dict:
         """
-        Walk directory tree.
-
-        **DEPRECATED**
+        Build meta dictionary while walking directory.
 
         Parameters
         ----------
         path : str
-            Path to file or directory.
+            Path to target file.
         """
         if os.path.isfile(path):
-            # Calculate Size and hashes for each file.
-            size = os.path.getsize(path)
+            file_size = os.path.getsize(path)
+            if self.hybrid:
+                self.files.append(
+                    {
+                        "length": file_size,
+                        "path": os.path.relpath(path, self.path).split(os.sep),
+                    }
+                )
 
-            if size == 0:
-                return {"": {"length": size}}
+            if file_size == 0:
+                return {"": {"length": file_size}}
 
             logger.debug("Hashing %s", str(path))
-            fhash = HasherV2(path, self.piece_length, self.progress)
+            hasher = FileHasher(
+                path, self.piece_length, progress=True, hybrid=self.hybrid
+            )
+            layers = bytearray()
+            for result in hasher:
+                if self.hybrid:
+                    layer_hash, piece = result
+                    self.pieces.extend(piece)
+                else:
+                    layer_hash = result
+                layers.extend(layer_hash)
+            if file_size > self.piece_length:
+                self.piece_layers[hasher.root] = layers
+            if self.hybrid and hasher.padding_file:
+                self.files.append(hasher.padding_file)
 
-            if size > self.piece_length:
-                self.piece_layers[fhash.root] = fhash.piece_layer
-            return {"": {"length": size, "pieces root": fhash.root}}
+            return {"": {"length": file_size, "pieces root": hasher.root}}
 
-        file_tree = {}
+        tree = {}
         if os.path.isdir(path):
             for name in sorted(os.listdir(path)):
-                file_tree[name] = self._traverse(os.path.join(path, name))
-        return file_tree
+                tree[name] = self._traverse(os.path.join(path, name))
+        return tree
 
- +
+
@@ -11734,214 +16708,267 @@

-
-
- -hasher (CbMixin, ProgMixin) - + + + +
+ + + +
+__init__(**kwargs) + +
+
-

Calculate the root hash and piece layers for file contents.

-

DEPRECATED

-

Iterates over 16KiB blocks of data from given file, hashes the data, -then creates a hash tree from the individual block hashes until size of -hashed data equals the piece-length. Then continues the hash tree until -root hash is calculated.

+

Create Bittorrent v1 v2 hybrid metafiles.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
pathstr

Path to file.

required
piece_lengthint

Size of layer hashes pieces.

required
progressbool

default = None

True
Source code in torrentfile\torrent.py -
class HasherV2(CbMixin, ProgMixin):
+          
658
+659
+660
+661
+662
+663
+664
+665
+666
+667
+668
+669
+670
+671
def __init__(self, **kwargs):
     """
-    Calculate the root hash and piece layers for file contents.
+    Create Bittorrent v1 v2 hybrid metafiles.
+    """
+    super().__init__(**kwargs)
+    logger.debug("Assembling bittorrent Hybrid file")
+    self.name = os.path.basename(self.path)
+    self.hashes = []
+    self.piece_layers = {}
+    self.pieces = bytearray()
+    self.files = []
+    self.hybrid = self.meta_version == "3"
+    self.total = len(utils.get_file_list(self.path))
+    self.assemble()
+
+
+
+
- **DEPRECATED** +
- Iterates over 16KiB blocks of data from given file, hashes the data, - then creates a hash tree from the individual block hashes until size of - hashed data equals the piece-length. Then continues the hash tree until - root hash is calculated. - Parameters - ---------- - path : str - Path to file. - piece_length : int - Size of layer hashes pieces. - progress : int - default = None - """ - def __init__(self, path: str, piece_length: int, progress: bool = True): - """ - Calculate and store hash information for specific file. +
- **DEPRECATED** - """ - self.path = path - self.root = None - self.piece_layer = None - self.layer_hashes = [] - self.piece_length = piece_length - self.num_blocks = piece_length // BLOCK_SIZE - if progress: - self.prog_start(os.path.getsize(path), path, unit="bytes") - with open(self.path, "rb") as fd: - self.process_file(fd) - def process_file(self, fd: str): - """ - Calculate hashes over 16KiB chuncks of file content. - **DEPRECATED** +
+_traverse(path) - Parameters - ---------- - fd : TextIOWrapper - Opened file in read mode. - """ - while True: - blocks = [] - leaf = bytearray(BLOCK_SIZE) - # generate leaves of merkle tree - - for _ in range(self.num_blocks): - size = fd.readinto(leaf) - self.prog_update(size) - if not size: - break - blocks.append(sha256(leaf[:size]).digest()) - - # blocks is empty mean eof - if not blocks: - break - if len(blocks) != self.num_blocks: - # when size of file doesn't fill the last block - # when the file contains multiple pieces - remaining = self.num_blocks - len(blocks) - if not self.layer_hashes: - # when the there is only one block for file - power2 = next_power_2(len(blocks)) - remaining = power2 - len(blocks) - # pad the the rest with zeroes to fill remaining space. - padding = [bytes(32) for _ in range(remaining)] - self.prog_update(HASH_SIZE * remaining) - blocks.extend(padding) - # calculate the root hash for the merkle tree up to piece-length +
- layer_hash = merkle_root(blocks) - if self._cb: - self._cb(layer_hash) - self.layer_hashes.append(layer_hash) - self._calculate_root() - self.prog_close() - def _calculate_root(self): - """ - Calculate root hash for the target file. +
- **DEPRECATED** - """ - self.piece_layer = b"".join(self.layer_hashes) - hashes = len(self.layer_hashes) - if hashes > 1: - pow2 = next_power_2(hashes) - remainder = pow2 - hashes - pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)] - for _ in range(remainder): - self.layer_hashes.append(merkle_root(pad_piece)) - self.root = merkle_root(self.layer_hashes) -
- +

Build meta dictionary while walking directory.

+

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

Path to target file.

+ required +
+
+ Source code in torrentfile\torrent.py +
694
+695
+696
+697
+698
+699
+700
+701
+702
+703
+704
+705
+706
+707
+708
+709
+710
+711
+712
+713
+714
+715
+716
+717
+718
+719
+720
+721
+722
+723
+724
+725
+726
+727
+728
+729
+730
+731
+732
+733
+734
+735
+736
+737
+738
+739
def _traverse(self, path: str) -> dict:
+    """
+    Build meta dictionary while walking directory.
 
-  
+ Parameters + ---------- + path : str + Path to target file. + """ + if os.path.isfile(path): + file_size = os.path.getsize(path) + if self.hybrid: + self.files.append( + { + "length": file_size, + "path": os.path.relpath(path, self.path).split(os.sep), + } + ) + if file_size == 0: + return {"": {"length": file_size}} + logger.debug("Hashing %s", str(path)) + hasher = FileHasher( + path, self.piece_length, progress=True, hybrid=self.hybrid + ) + layers = bytearray() + for result in hasher: + if self.hybrid: + layer_hash, piece = result + self.pieces.extend(piece) + else: + layer_hash = result + layers.extend(layer_hash) + if file_size > self.piece_length: + self.piece_layers[hasher.root] = layers + if self.hybrid and hasher.padding_file: + self.files.append(hasher.padding_file) + return {"": {"length": file_size, "pieces root": hasher.root}} + tree = {} + if os.path.isdir(path): + for name in sorted(os.listdir(path)): + tree[name] = self._traverse(os.path.join(path, name)) + return tree +
+
+
+
+
+
-
+
+assemble() -
-__init__(self, path, piece_length, progress=True) - - special - +
-

-

Calculate and store hash information for specific file.

-

DEPRECATED

+

Assemble the parts of the torrentfile into meta dictionary.

Source code in torrentfile\torrent.py -
def __init__(self, path: str, piece_length: int, progress: bool = True):
+          
673
+674
+675
+676
+677
+678
+679
+680
+681
+682
+683
+684
+685
+686
+687
+688
+689
+690
+691
+692
def assemble(self):
     """
-    Calculate and store hash information for specific file.
-
-    **DEPRECATED**
+    Assemble the parts of the torrentfile into meta dictionary.
     """
-    self.path = path
-    self.root = None
-    self.piece_layer = None
-    self.layer_hashes = []
-    self.piece_length = piece_length
-    self.num_blocks = piece_length // BLOCK_SIZE
-    if progress:
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
-    with open(self.path, "rb") as fd:
-        self.process_file(fd)
+    info = self.meta["info"]
+    info["meta version"] = 2
+
+    if os.path.isfile(self.path):
+        info["file tree"] = {self.name: self._traverse(self.path)}
+        info["length"] = os.path.getsize(self.path)
+
+    else:
+        info["file tree"] = self._traverse(self.path)
+        if self.hybrid:
+            info["files"] = self.files
+
+    if self.hybrid:
+        info["pieces"] = self.pieces
+    self.meta["piece layers"] = self.piece_layers
+    return info
 
+
@@ -11949,166 +16976,254 @@
+
+ +
-
-process_file(self, fd) +
+ + + +
-

+ +

+ TorrentFile + + + +

+
+

+ Bases: MetaFile, ProgMixin

-

Calculate hashes over 16KiB chuncks of file content.

-

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Class for creating Bittorrent meta files.

+

Construct Torrentfile class instance object.

+ +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
fdstr

Opened file in read mode.

requiredNameTypeDescriptionDefault
-
- Source code in torrentfile\torrent.py -
def process_file(self, fd: str):
+    
+    
+        
+          kwargs
+          
+                dict
+          
+          

Dictionary containing torrent file options.

+ + required + + + + + + +
+ Source code in torrentfile\torrent.py +
405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
class TorrentFile(MetaFile, ProgMixin):
     """
-    Calculate hashes over 16KiB chuncks of file content.
+    Class for creating Bittorrent meta files.
 
-    **DEPRECATED**
+    Construct *Torrentfile* class instance object.
 
     Parameters
     ----------
-    fd : TextIOWrapper
-        Opened file in read mode.
+    kwargs : dict
+        Dictionary containing torrent file options.
     """
-    while True:
-        blocks = []
-        leaf = bytearray(BLOCK_SIZE)
-        # generate leaves of merkle tree
 
-        for _ in range(self.num_blocks):
-            size = fd.readinto(leaf)
-            self.prog_update(size)
-            if not size:
-                break
-            blocks.append(sha256(leaf[:size]).digest())
+    hasher = Hasher
 
-        # blocks is empty mean eof
-        if not blocks:
-            break
-        if len(blocks) != self.num_blocks:
-            # when size of file doesn't fill the last block
-            # when the file contains multiple pieces
-            remaining = self.num_blocks - len(blocks)
-            if not self.layer_hashes:
-                # when the there is only one block for file
-                power2 = next_power_2(len(blocks))
-                remaining = power2 - len(blocks)
+    def __init__(self, **kwargs):
+        """
+        Construct TorrentFile instance with given keyword args.
 
-            # pad the the rest with zeroes to fill remaining space.
-            padding = [bytes(32) for _ in range(remaining)]
-            self.prog_update(HASH_SIZE * remaining)
-            blocks.extend(padding)
-        # calculate the root hash for the merkle tree up to piece-length
+        Parameters
+        ----------
+        kwargs : dict
+            dictionary of keyword args passed to superclass.
+        """
+        super().__init__(**kwargs)
+        logger.debug("Assembling bittorrent v1 torrent file")
+        self.assemble()
 
-        layer_hash = merkle_root(blocks)
-        if self._cb:
-            self._cb(layer_hash)
-        self.layer_hashes.append(layer_hash)
-    self._calculate_root()
-    self.prog_close()
-
- - + def assemble(self): + """ + Assemble components of torrent metafile. - + Returns + ------- + dict + metadata dictionary for torrent file + """ + info = self.meta["info"] + size, filelist = utils.filelist_total(self.path) + if os.path.isfile(self.path): + info["length"] = size + else: + info["files"] = [ + { + "length": os.path.getsize(path), + "path": os.path.relpath(path, self.path).split(os.sep), + } + for path in filelist + ] + pieces = bytearray() + + feeder = Hasher(filelist, self.piece_length, self.progress) + for piece in feeder: + pieces.extend(piece) + + info["pieces"] = pieces + +
+
+
-
-
-
-
+
-
-__init__(self, **kwargs) +
+__init__(**kwargs) - - special -
+
-

Construct TorrentFileV2 Class instance from given parameters.

-

DEPRECATED

+

Construct TorrentFile instance with given keyword args.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
kwargsdict

keywword arguments to pass to superclass.

{}NameTypeDescriptionDefault
+ + + + kwargs + + dict + +

dictionary of keyword args passed to superclass.

+ + required + + + + +
Source code in torrentfile\torrent.py -
def __init__(self, **kwargs):
+          
419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
def __init__(self, **kwargs):
     """
-    Construct `TorrentFileV2` Class instance from given parameters.
-
-    **DEPRECATED**
+    Construct TorrentFile instance with given keyword args.
 
     Parameters
     ----------
     kwargs : dict
-        keywword arguments to pass to superclass.
+        dictionary of keyword args passed to superclass.
     """
     super().__init__(**kwargs)
-    logger.debug("Assembling bittorrent v2 torrent file")
-    self.piece_layers = {}
-    self.hashes = []
-    self.total = len(utils.get_file_list(self.path))
+    logger.debug("Assembling bittorrent v1 torrent file")
     self.assemble()
 
+
@@ -12116,60 +17231,98 @@
-
+
-
-assemble(self) +
+assemble()
+
-

Assemble then return the meta dictionary for encoding.

-

DEPRECATED

+

Assemble components of torrent metafile.

+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ dict +

metadata dictionary for torrent file

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
dict

Metainformation about the torrent.

Source code in torrentfile\torrent.py -
def assemble(self):
+          
432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
def assemble(self):
     """
-    Assemble then return the meta dictionary for encoding.
-
-    **DEPRECATED**
+    Assemble components of torrent metafile.
 
     Returns
     -------
-    meta : dict
-        Metainformation about the torrent.
+    dict
+        metadata dictionary for torrent file
     """
     info = self.meta["info"]
+    size, filelist = utils.filelist_total(self.path)
     if os.path.isfile(self.path):
-        info["file tree"] = {info["name"]: self._traverse(self.path)}
-        info["length"] = os.path.getsize(self.path)
-        self.prog_update(info["length"])
+        info["length"] = size
     else:
-        info["file tree"] = self._traverse(self.path)
+        info["files"] = [
+            {
+                "length": os.path.getsize(path),
+                "path": os.path.relpath(path, self.path).split(os.sep),
+            }
+            for path in filelist
+        ]
+    pieces = bytearray()
 
-    info["meta version"] = 2
-    self.meta["piece layers"] = self.piece_layers
+    feeder = Hasher(filelist, self.piece_length, self.progress)
+    for piece in feeder:
+        pieces.extend(piece)
+
+    info["pieces"] = pieces
 
+
@@ -12187,237 +17340,304 @@
- - - - -
- -
- -
- - - -
- - - -

- utils - - - -

- -
- -

Utility functions and classes used throughout package.

-

Functions: - get_piece_length: calculate ideal piece length for torrent file. - sortfiles: traverse directory in sorted order yielding paths encountered. - path_size: Sum the sizes of each file in path. - get_file_list: Return list of all files contained in directory. - path_stat: Get ideal piece length, total size, and file list for directory. - path_piece_length: Get ideal piece length based on size of directory.

-

Classes: - MissingPathError: Custom exception raised when no path was provided to CLI. - PieceLengthValueError: Custom exception raised when incorrect input value - used for piece length field.

- - - -
- - - - - - -
-

- -Memo +

+ TorrentFileHybrid

+
+

+ Bases: MetaFile, ProgMixin

-

Memoice chache object.

-

Parameters:

- - - - - - - - - - +

Construct the Hybrid torrent meta file with provided parameters.

+

DEPRECATED

+ +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
funcfunction

The function that is being memoized.

requiredNameTypeDescriptionDefault
-
- Source code in torrentfile\utils.py -
class Memo:
+    
+    
+        
+          kwargs
+          
+                dict
+          
+          

Keyword arguments for torrent options.

+ + required + + + + + + +
+ Source code in torrentfile\torrent.py +
548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
class TorrentFileHybrid(MetaFile, ProgMixin):
     """
-    Memoice chache object.
+    Construct the Hybrid torrent meta file with provided parameters.
+
+    **DEPRECATED**
 
     Parameters
     ----------
-    func : function
-        The function that is being memoized.
+    kwargs : dict
+        Keyword arguments for torrent options.
     """
 
-    def __init__(self, func):
+    hasher = HasherHybrid
+
+    def __init__(self, **kwargs):
         """
-        Construct for memoization.
+        Create Bittorrent v1 v2 hybrid metafiles.
         """
-        self.func = func
-        self.counter = 0
-        self.cache = {}
+        super().__init__(**kwargs)
+        logger.debug("Assembling bittorrent Hybrid file")
+        self.name = os.path.basename(self.path)
+        self.hashes = []
+        self.piece_layers = {}
+        self.pieces = []
+        self.files = []
+        self.total = len(utils.get_file_list(self.path))
+        self.assemble()
 
-    def __call__(self, path):
+    def assemble(self):
         """
-        Invoke each time memo function is called.
+        Assemble the parts of the torrentfile into meta dictionary.
+
+        **DEPRECATED**
+        """
+        info = self.meta["info"]
+        info["meta version"] = 2
+
+        if os.path.isfile(self.path):
+            info["file tree"] = {self.name: self._traverse(self.path)}
+            info["length"] = os.path.getsize(self.path)
+
+        else:
+            info["file tree"] = self._traverse(self.path)
+            info["files"] = self.files
+
+        info["pieces"] = b"".join(self.pieces)
+        self.meta["piece layers"] = self.piece_layers
+        return info
+
+    def _traverse(self, path: str) -> dict:
+        """
+        Build meta dictionary while walking directory.
+
+        **DEPRECATED**
 
         Parameters
         ----------
         path : str
-            The relative or absolute path being used as key in cache dict.
-
-        Returns
-        -------
-        Any :
-            The results of calling the function with path.
+            Path to target file.
         """
-        if path in self.cache and os.path.exists(path):
-            self.counter += 1
-            return self.cache[path]
-        result = self.func(path)
-        self.cache[path] = result
-        return result
-
- + if os.path.isfile(path): + file_size = os.path.getsize(path) + self.files.append( + { + "length": file_size, + "path": os.path.relpath(path, self.path).split(os.sep), + } + ) + if file_size == 0: + return {"": {"length": file_size}} -
+ logger.debug("Hashing %s", str(path)) + file_hash = HasherHybrid(path, self.piece_length, self.progress) + self.prog_update(file_size) + if file_size > self.piece_length: + self.piece_layers[file_hash.root] = file_hash.piece_layer + self.hashes.append(file_hash) + self.pieces.extend(file_hash.pieces) + if file_hash.padding_file: + self.files.append(file_hash.padding_file) + return {"": {"length": file_size, "pieces root": file_hash.root}} + tree = {} + if os.path.isdir(path): + for name in sorted(os.listdir(path)): + tree[name] = self._traverse(os.path.join(path, name)) + return tree +
+
+
+
-
-
-__call__(self, path) - - special - -
-
-

Invoke each time memo function is called.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
pathstr

The relative or absolute path being used as key in cache dict.

required
-
- Source code in torrentfile\utils.py -
def __call__(self, path):
-    """
-    Invoke each time memo function is called.
 
-    Parameters
-    ----------
-    path : str
-        The relative or absolute path being used as key in cache dict.
 
-    Returns
-    -------
-    Any :
-        The results of calling the function with path.
-    """
-    if path in self.cache and os.path.exists(path):
-        self.counter += 1
-        return self.cache[path]
-    result = self.func(path)
-    self.cache[path] = result
-    return result
-
-
-
-
-
+
-
-__init__(self, func) +
+__init__(**kwargs) - - special -
+
-

Construct for memoization.

+

Create Bittorrent v1 v2 hybrid metafiles.

- Source code in torrentfile\utils.py -
def __init__(self, func):
+          Source code in torrentfile\torrent.py
+          
562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
def __init__(self, **kwargs):
     """
-    Construct for memoization.
+    Create Bittorrent v1 v2 hybrid metafiles.
     """
-    self.func = func
-    self.counter = 0
-    self.cache = {}
+    super().__init__(**kwargs)
+    logger.debug("Assembling bittorrent Hybrid file")
+    self.name = os.path.basename(self.path)
+    self.hashes = []
+    self.piece_layers = {}
+    self.pieces = []
+    self.files = []
+    self.total = len(utils.get_file_list(self.path))
+    self.assemble()
 
+
@@ -12425,119 +17645,203 @@
+
-
-
+
+_traverse(path) -
+ -
+
+

Build meta dictionary while walking directory.

+

DEPRECATED

+

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

Path to target file.

+ required +
-

- -MissingPathError (Exception) - +
+ Source code in torrentfile\torrent.py +
597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
def _traverse(self, path: str) -> dict:
+    """
+    Build meta dictionary while walking directory.
 
-
-
-
-
-    
- -

Path parameter is required to specify target content.

-

Creating a .torrent file with no contents seems rather silly.

- -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
messagestr

Message for user (optional).

None
-
- Source code in torrentfile\utils.py -
class MissingPathError(Exception):
-    """
-    Path parameter is required to specify target content.
-
-    Creating a .torrent file with no contents seems rather silly.
+    **DEPRECATED**
 
     Parameters
     ----------
-    message : str
-        Message for user (optional).
+    path : str
+        Path to target file.
     """
+    if os.path.isfile(path):
+        file_size = os.path.getsize(path)
 
-    def __init__(self, message: str = None):
-        """
-        Raise when creating a meta file without specifying target content.
-
-        The `message` argument is a message to pass to Exception base class.
-        """
-        self.message = f"Path arguement is missing and required {str(message)}"
-        super().__init__(message)
-
-
- + self.files.append( + { + "length": file_size, + "path": os.path.relpath(path, self.path).split(os.sep), + } + ) + if file_size == 0: + return {"": {"length": file_size}} -
+ logger.debug("Hashing %s", str(path)) + file_hash = HasherHybrid(path, self.piece_length, self.progress) + self.prog_update(file_size) + if file_size > self.piece_length: + self.piece_layers[file_hash.root] = file_hash.piece_layer + self.hashes.append(file_hash) + self.pieces.extend(file_hash.pieces) + if file_hash.padding_file: + self.files.append(file_hash.padding_file) + return {"": {"length": file_size, "pieces root": file_hash.root}} + tree = {} + if os.path.isdir(path): + for name in sorted(os.listdir(path)): + tree[name] = self._traverse(os.path.join(path, name)) + return tree +
+
+
+

+
-
+
-
-__init__(self, message=None) +
+assemble() - - special -
+
-

Raise when creating a meta file without specifying target content.

-

The message argument is a message to pass to Exception base class.

+

Assemble the parts of the torrentfile into meta dictionary.

+

DEPRECATED

- Source code in torrentfile\utils.py -
def __init__(self, message: str = None):
+          Source code in torrentfile\torrent.py
+          
576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
def assemble(self):
     """
-    Raise when creating a meta file without specifying target content.
+    Assemble the parts of the torrentfile into meta dictionary.
 
-    The `message` argument is a message to pass to Exception base class.
+    **DEPRECATED**
     """
-    self.message = f"Path arguement is missing and required {str(message)}"
-    super().__init__(message)
+    info = self.meta["info"]
+    info["meta version"] = 2
+
+    if os.path.isfile(self.path):
+        info["file tree"] = {self.name: self._traverse(self.path)}
+        info["length"] = os.path.getsize(self.path)
+
+    else:
+        info["file tree"] = self._traverse(self.path)
+        info["files"] = self.files
+
+    info["pieces"] = b"".join(self.pieces)
+    self.meta["piece layers"] = self.piece_layers
+    return info
 
+
@@ -12559,116 +17863,231 @@
-

- -PieceLengthValueError (Exception) - +

+ TorrentFileV2

+
+

+ Bases: MetaFile, ProgMixin

-

Piece Length parameter must equal a perfect power of 2.

-

Parameters:

- - - - - - - - - - +

Class for creating Bittorrent meta v2 files.

+

DEPRECATED

+ +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
messagestr

Message for user (optional).

NoneNameTypeDescriptionDefault
-
- Source code in torrentfile\utils.py -
class PieceLengthValueError(Exception):
+    
+    
+        
+          kwargs
+          
+                dict
+          
+          

Keyword arguments for torrent file options.

+ + required + + + + + + +
+ Source code in torrentfile\torrent.py +
462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
class TorrentFileV2(MetaFile, ProgMixin):
     """
-    Piece Length parameter must equal a perfect power of 2.
+    Class for creating Bittorrent meta v2 files.
+
+    **DEPRECATED**
 
     Parameters
     ----------
-    message : str
-        Message for user (optional).
+    kwargs : dict
+        Keyword arguments for torrent file options.
     """
 
-    def __init__(self, message: str = None):
-        """
-        Raise when creating a meta file with incorrect piece length value.
-
-        The `message` argument is a message to pass to Exception base class.
-        """
-        self.message = f"Incorrect value for piece length: {str(message)}"
-        super().__init__(message)
-
- - - - -
- + hasher = HasherV2 + def __init__(self, **kwargs): + """ + Construct `TorrentFileV2` Class instance from given parameters. + **DEPRECATED** + Parameters + ---------- + kwargs : dict + keywword arguments to pass to superclass. + """ + super().__init__(**kwargs) + logger.debug("Assembling bittorrent v2 torrent file") + self.piece_layers = {} + self.hashes = [] + self.total = len(utils.get_file_list(self.path)) + self.assemble() + def assemble(self): + """ + Assemble then return the meta dictionary for encoding. + **DEPRECATED** + Returns + ------- + meta : dict + Metainformation about the torrent. + """ + info = self.meta["info"] + if os.path.isfile(self.path): + info["file tree"] = {info["name"]: self._traverse(self.path)} + info["length"] = os.path.getsize(self.path) + self.prog_update(info["length"]) + else: + info["file tree"] = self._traverse(self.path) + info["meta version"] = 2 + self.meta["piece layers"] = self.piece_layers -
+ def _traverse(self, path: str) -> dict: + """ + Walk directory tree. + **DEPRECATED** + Parameters + ---------- + path : str + Path to file or directory. + """ + if os.path.isfile(path): + # Calculate Size and hashes for each file. + size = os.path.getsize(path) -
-__init__(self, message=None) + if size == 0: + return {"": {"length": size}} - - special - + logger.debug("Hashing %s", str(path)) + fhash = HasherV2(path, self.piece_length, self.progress) -
+ if size > self.piece_length: + self.piece_layers[fhash.root] = fhash.piece_layer + return {"": {"length": size, "pieces root": fhash.root}} -
+ file_tree = {} + if os.path.isdir(path): + for name in sorted(os.listdir(path)): + file_tree[name] = self._traverse(os.path.join(path, name)) + return file_tree +
+
+
-

Raise when creating a meta file with incorrect piece length value.

-

The message argument is a message to pass to Exception base class.

-
- Source code in torrentfile\utils.py -
def __init__(self, message: str = None):
-    """
-    Raise when creating a meta file with incorrect piece length value.
 
-    The `message` argument is a message to pass to Exception base class.
-    """
-    self.message = f"Incorrect value for piece length: {str(message)}"
-    super().__init__(message)
-
-
-
+
-
-
-
-
@@ -12677,69 +18096,79 @@
-get_file_list(path) +
+__init__(**kwargs) -
+ +
-

Return a sorted list of file paths contained in directory.

+

Construct TorrentFileV2 Class instance from given parameters.

+

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
pathstr

target file or directory.

requiredNameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
list

sorted list of file paths.

+ + + + kwargs + + dict + +

keywword arguments to pass to superclass.

+ + required + + + + +
- Source code in torrentfile\utils.py -
def get_file_list(path: str) -> list:
+          Source code in torrentfile\torrent.py
+          
476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
def __init__(self, **kwargs):
     """
-    Return a sorted list of file paths contained in directory.
+    Construct `TorrentFileV2` Class instance from given parameters.
+
+    **DEPRECATED**
 
     Parameters
     ----------
-    path : str
-        target file or directory.
-
-    Returns
-    -------
-    list
-        sorted list of file paths.
+    kwargs : dict
+        keywword arguments to pass to superclass.
     """
-    _, filelist = filelist_total(path)
-    return filelist
+    super().__init__(**kwargs)
+    logger.debug("Assembling bittorrent v2 torrent file")
+    self.piece_layers = {}
+    self.hashes = []
+    self.total = len(utils.get_file_list(self.path))
+    self.assemble()
 
+
@@ -12751,71 +18180,105 @@

-

-get_piece_length(size) +

+_traverse(path) -
+ +
-

Calculate the ideal piece length for bittorrent data.

+

Walk directory tree.

+

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
sizeint

Total bits of all files incluided in .torrent file.

requiredNameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

Ideal piece length.

+ + + + path + + str + +

Path to file or directory.

+ + required + + + + +
- Source code in torrentfile\utils.py -
def get_piece_length(size: int) -> int:
-    """
-    Calculate the ideal piece length for bittorrent data.
+          Source code in torrentfile\torrent.py
+          
516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
def _traverse(self, path: str) -> dict:
+    """
+    Walk directory tree.
+
+    **DEPRECATED**
 
     Parameters
     ----------
-    size : int
-        Total bits of all files incluided in .torrent file.
-
-    Returns
-    -------
-    int
-        Ideal piece length.
+    path : str
+        Path to file or directory.
     """
-    exp = 14
-    while size / (2**exp) > 200 and exp < 25:
-        exp += 1
-    return 2**exp
+    if os.path.isfile(path):
+        # Calculate Size and hashes for each file.
+        size = os.path.getsize(path)
+
+        if size == 0:
+            return {"": {"length": size}}
+
+        logger.debug("Hashing %s", str(path))
+        fhash = HasherV2(path, self.piece_length, self.progress)
+
+        if size > self.piece_length:
+            self.piece_layers[fhash.root] = fhash.piece_layer
+        return {"": {"length": size, "pieces root": fhash.root}}
+
+    file_tree = {}
+    if os.path.isdir(path):
+        for name in sorted(os.listdir(path)):
+            file_tree[name] = self._traverse(os.path.join(path, name))
+    return file_tree
 
+
@@ -12827,74 +18290,81 @@

-

-humanize_bytes(amount) +

+assemble() -
+ +
-

Convert integer into human readable memory sized denomination.

+

Assemble then return the meta dictionary for encoding.

+

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Returns:

+
NameTypeDescriptionDefault
+ - - - - + + - -
amountint

total number of bytes.

requiredName TypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

human readable representation of the given amount of bytes.

+ + + +meta + dict + +

Metainformation about the torrent.

+ + + +
- Source code in torrentfile\utils.py -
def humanize_bytes(amount: int) -> str:
+          Source code in torrentfile\torrent.py
+          
494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
def assemble(self):
     """
-    Convert integer into human readable memory sized denomination.
+    Assemble then return the meta dictionary for encoding.
 
-    Parameters
-    ----------
-    amount : int
-        total number of bytes.
+    **DEPRECATED**
 
     Returns
     -------
-    str
-        human readable representation of the given amount of bytes.
+    meta : dict
+        Metainformation about the torrent.
     """
-    if amount < 1024:
-        return str(amount)
-    if 1024 <= amount < 1_048_576:
-        return f"{amount // 1024} KiB"
-    if 1_048_576 <= amount < 1_073_741_824:
-        return f"{amount // 1_048_576} MiB"
-    return f"{amount // 1073741824} GiB"
+    info = self.meta["info"]
+    if os.path.isfile(self.path):
+        info["file tree"] = {info["name"]: self._traverse(self.path)}
+        info["length"] = os.path.getsize(self.path)
+        self.prog_update(info["length"])
+    else:
+        info["file tree"] = self._traverse(self.path)
+
+    info["meta version"] = 2
+    self.meta["piece layers"] = self.piece_layers
 
+
@@ -12902,336 +18372,197 @@

-
+
-

-next_power_2(value) +

+
- -
-

Calculate the next perfect power of 2 equal to or greater than value.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
valueint

integer value that is less than some perfect power of 2.

required
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

The next power of 2 greater than value, or value if already power of 2.

-
- Source code in torrentfile\utils.py -
def next_power_2(value: int) -> int:
-    """
-    Calculate the next perfect power of 2 equal to or greater than value.
 
-    Parameters
-    ----------
-    value : int
-        integer value that is less than some perfect power of 2.
 
-    Returns
-    -------
-    int
-        The next power of 2 greater than value, or value if already power of 2.
-    """
-    if not value & (value - 1) and value:
-        return value
-    start = 1
-    while start < value:
-        start <<= 1
-    return start
-
-
+
+
-
+
-

-normalize_piece_length(piece_length) +

+ utils -

+ +
-

Verify input piece_length is valid and convert accordingly.

+

Utility functions and classes used throughout package.

+

Functions: + get_piece_length: calculate ideal piece length for torrent file. + sortfiles: traverse directory in sorted order yielding paths encountered. + path_size: Sum the sizes of each file in path. + get_file_list: Return list of all files contained in directory. + path_stat: Get ideal piece length, total size, and file list for directory. + path_piece_length: Get ideal piece length based on size of directory.

+

Classes: + MissingPathError: Custom exception raised when no path was provided to CLI. + PieceLengthValueError: Custom exception raised when incorrect input value + used for piece length field.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
piece_lengthint

The piece length provided by user.

required
-

Exceptions:

- - - - - - - - - - - - - -
TypeDescription
PieceLengthValueError :

If piece length is improper value.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

normalized piece length.

-
- Source code in torrentfile\utils.py -
def normalize_piece_length(piece_length: int) -> int:
-    """
-    Verify input piece_length is valid and convert accordingly.
 
-    Parameters
-    ----------
-    piece_length : int | str
-        The piece length provided by user.
 
-    Returns
-    -------
-    int
-        normalized piece length.
+  
- Raises - ------ - PieceLengthValueError : - If piece length is improper value. - """ - if isinstance(piece_length, str): - if piece_length.isnumeric(): - piece_length = int(piece_length) - else: - raise PieceLengthValueError(piece_length) - if 13 < piece_length < 26: - return 2**piece_length - if piece_length <= 13: - raise PieceLengthValueError(piece_length) - log = int(math.log2(piece_length)) - if 2**log == piece_length: - return piece_length - raise PieceLengthValueError -
-
-
-
-
+
-

-path_piece_length(path) +

+ Memo +

+
-

Calculate piece length for input path and contents.

-

Parameters:

- - - - - - - - - - +

Memoice chache object.

+ +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
pathstr

The absolute path to directory and contents.

requiredNameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

The size of pieces of torrent content.

-
- Source code in torrentfile\utils.py -
def path_piece_length(path: str) -> int:
+    
+    
+        
+          func
+          
+                function
+          
+          

The function that is being memoized.

+ + required + + + + + + +
+ Source code in torrentfile\utils.py +
41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
class Memo:
     """
-    Calculate piece length for input path and contents.
+    Memoice chache object.
 
     Parameters
     ----------
-    path : str
-        The absolute path to directory and contents.
-
-    Returns
-    -------
-    int
-        The size of pieces of torrent content.
+    func : function
+        The function that is being memoized.
     """
-    psize = path_size(path)
-    return get_piece_length(psize)
-
- - - + def __init__(self, func): + """ + Construct for memoization. + """ + self.func = func + self.counter = 0 + self.cache = {} + def __call__(self, path): + """ + Invoke each time memo function is called. + Parameters + ---------- + path : str + The relative or absolute path being used as key in cache dict. -
+ Returns + ------- + Any : + The results of calling the function with path. + """ + if path in self.cache and os.path.exists(path): + self.counter += 1 + return self.cache[path] + result = self.func(path) + self.cache[path] = result + return result +
+
+
-

-path_size(path) +
-

-
-

Return the total size of all files in path recursively.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
pathstr

path to target file or directory.

required
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

total size of files.

-
- Source code in torrentfile\utils.py -
def path_size(path: str) -> int:
-    """
-    Return the total size of all files in path recursively.
 
-    Parameters
-    ----------
-    path : str
-        path to target file or directory.
 
-    Returns
-    -------
-    int
-        total size of files.
-    """
-    total_size, _ = filelist_total(path)
-    return total_size
-
-
-
-
@@ -13239,74 +18570,101 @@

-

-path_stat(path) +

+__call__(path) -
+ +
-

Calculate directory statistics.

+

Invoke each time memo function is called.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

The relative or absolute path being used as key in cache dict.

+ required +
+ +

Returns:

+ + - - - - + + - -
pathstr

The path to start calculating from.

requiredName TypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
tuple

List of all files contained in Directory

+ + + +Any + +

The results of calling the function with path.

+ + + +
Source code in torrentfile\utils.py -
def path_stat(path: str) -> tuple:
+          
59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
def __call__(self, path):
     """
-    Calculate directory statistics.
+    Invoke each time memo function is called.
 
     Parameters
     ----------
     path : str
-        The path to start calculating from.
+        The relative or absolute path being used as key in cache dict.
 
     Returns
     -------
-    list
-        List of all files contained in Directory
-    int
-        Total sum of bytes from all contents of dir
-    int
-        The size of pieces of the torrent contents.
+    Any :
+        The results of calling the function with path.
     """
-    total_size, filelist = filelist_total(path)
-    piece_length = get_piece_length(total_size)
-    return (filelist, total_size, piece_length)
+    if path in self.cache and os.path.exists(path):
+        self.counter += 1
+        return self.cache[path]
+    result = self.func(path)
+    self.cache[path] = result
+    return result
 
+
@@ -13314,213 +18672,293 @@

+
-
- -

+
+__init__(func) -
+ -
+
+

Construct for memoization.

+
+ Source code in torrentfile\utils.py +
51
+52
+53
+54
+55
+56
+57
def __init__(self, func):
+    """
+    Construct for memoization.
+    """
+    self.func = func
+    self.counter = 0
+    self.cache = {}
+
+
+
+
-

- version +

- -
-

Holds the release version number.

+
+
+
-
+
+

+ MissingPathError +

+
+

+ Bases: Exception

-
+

Path parameter is required to specify target content.

+

Creating a .torrent file with no contents seems rather silly.

-
+

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
message + str +

Message for user (optional).

+ None +
+ + +
+ Source code in torrentfile\utils.py +
 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
class MissingPathError(Exception):
+    """
+    Path parameter is required to specify target content.
 
-  
+ Creating a .torrent file with no contents seems rather silly. + Parameters + ---------- + message : str + Message for user (optional). + """ + def __init__(self, message: str = None): + """ + Raise when creating a meta file without specifying target content. + The `message` argument is a message to pass to Exception base class. + """ + self.message = f"Path arguement is missing and required {str(message)}" + super().__init__(message) + +
+
-
-
-
+
-
-

- torrentfile.cli -

+
-
-

Command Line Interface for TorrentFile project.

-

This module provides the primary command line argument parser for -the torrentfile package. The main_script function is automatically -invoked when called from command line, and parses accompanying arguments.

-

Functions: - main_script: process command line arguments and run program. - activate_logger: turns on debug mode and logging facility.

+
+__init__(message=None) -
+
+
+

Raise when creating a meta file without specifying target content.

+

The message argument is a message to pass to Exception base class.

+
+ Source code in torrentfile\utils.py +
 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
def __init__(self, message: str = None):
+    """
+    Raise when creating a meta file without specifying target content.
 
+    The `message` argument is a message to pass to Exception base class.
+    """
+    self.message = f"Path arguement is missing and required {str(message)}"
+    super().__init__(message)
+
+
+
+
+
-
-

- -TorrentFileHelpFormatter (HelpFormatter) - +

+
- +
-
-

Formatting class for help tips provided by the CLI.

-

Subclasses Argparse.HelpFormatter.

-
- Source code in torrentfile\cli.py -
class TorrentFileHelpFormatter(HelpFormatter):
-    """
-    Formatting class for help tips provided by the CLI.
+  
- Subclasses Argparse.HelpFormatter. - """ - def __init__(self, prog, width=40, max_help_positions=30): - """ - Construct HelpFormat class for usage output. - Parameters - ---------- - prog : str - Name of the program. - width : int - Max width of help message output. - max_help_positions : int - max length until line wrap. - """ - super().__init__( - prog, width=width, max_help_position=max_help_positions - ) +

+ PieceLengthValueError - def _split_lines(self, text, _): - """ - Split multiline help messages and remove indentation. - Parameters - ---------- - text : str - text that needs to be split - _ : int - max width for line. - """ - lines = text.split("\n") - return [line.strip() for line in lines if line] - def _format_text(self, text): - """ - Format text for cli usage messages. +

- Parameters - ---------- - text : str - Pre-formatted text. - Returns - ------- - str - Formatted text from input. - """ - text = text % dict(prog=self._prog) if "%(prog)" in text else text - text = self._whitespace_matcher.sub(" ", text).strip() - return text + "\n\n" +
+

+ Bases: Exception

- def _join_parts(self, part_strings): - """ - Combine different sections of the help message. - Parameters - ---------- - part_strings : list - List of argument help messages and headers. +

Piece Length parameter must equal a perfect power of 2.

- Returns - ------- - str - Fully formatted help message for CLI. - """ - parts = self.format_headers(part_strings) - return super()._join_parts(parts) +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
message + str +

Message for user (optional).

+ None +
+ + +
+ Source code in torrentfile\utils.py +
103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
class PieceLengthValueError(Exception):
+    """
+    Piece Length parameter must equal a perfect power of 2.
 
-    @staticmethod
-    def format_headers(parts):
-        """
-        Format help message section headers.
+    Parameters
+    ----------
+    message : str
+        Message for user (optional).
+    """
 
-        Parameters
-        ----------
-        parts : list
-            List of individual lines for help message.
+    def __init__(self, message: str = None):
+        """
+        Raise when creating a meta file with incorrect piece length value.
 
-        Returns
-        -------
-        list
-            Input list with formatted section headers.
+        The `message` argument is a message to pass to Exception base class.
         """
-        if parts and parts[0].startswith("usage:"):
-            parts[0] = "Usage\n=====\n  " + parts[0][6:]
-        headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
-        for i in headings[::-1]:
-            parts[i] = parts[i][:-2].title()
-            underline = "".join(["\n", "-" * len(parts[i]), "\n"])
-            parts.insert(i + 1, underline)
-        return parts
+        self.message = f"Incorrect value for piece length: {str(message)}"
+        super().__init__(message)
 
- +
+
@@ -13534,73 +18972,41 @@

-
+
-

-__init__(self, prog, width=40, max_help_positions=30) +

+__init__(message=None) - - special - -
+

+
-

Construct HelpFormat class for usage output.

+

Raise when creating a meta file with incorrect piece length value.

+

The message argument is a message to pass to Exception base class.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
progstr

Name of the program.

required
widthint

Max width of help message output.

40
max_help_positionsint

max length until line wrap.

30
- Source code in torrentfile\cli.py -
def __init__(self, prog, width=40, max_help_positions=30):
+          Source code in torrentfile\utils.py
+          
113
+114
+115
+116
+117
+118
+119
+120
def __init__(self, message: str = None):
     """
-    Construct HelpFormat class for usage output.
+    Raise when creating a meta file with incorrect piece length value.
 
-    Parameters
-    ----------
-    prog : str
-        Name of the program.
-    width : int
-        Max width of help message output.
-    max_help_positions : int
-        max length until line wrap.
+    The `message` argument is a message to pass to Exception base class.
     """
-    super().__init__(
-        prog, width=width, max_help_position=max_help_positions
-    )
+    self.message = f"Incorrect value for piece length: {str(message)}"
+    super().__init__(message)
 
+
@@ -13608,83 +19014,137 @@

+

+ +
+ +
+ -

-format_headers(parts) - - staticmethod - + +
+ + + +

+_filelist_total(path) +

+
-

Format help message section headers.

+

Search directory tree for files.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
partslist

List of individual lines for help message.

requiredNameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
list

Input list with formatted section headers.

+ + + + path + + str + +

Path to file or directory base

+ + required + + + + + +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ int +

Sum of all filesizes in filelist.

+ list +

All file paths within directory tree.

+
- Source code in torrentfile\cli.py -
@staticmethod
-def format_headers(parts):
-    """
-    Format help message section headers.
+          Source code in torrentfile\utils.py
+          
228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
def _filelist_total(path: str) -> tuple:
+    """
+    Search directory tree for files.
 
     Parameters
     ----------
-    parts : list
-        List of individual lines for help message.
+    path : str
+        Path to file or directory base
 
     Returns
     -------
+    int
+        Sum of all filesizes in filelist.
     list
-        Input list with formatted section headers.
+        All file paths within directory tree.
     """
-    if parts and parts[0].startswith("usage:"):
-        parts[0] = "Usage\n=====\n  " + parts[0][6:]
-    headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
-    for i in headings[::-1]:
-        parts[i] = parts[i][:-2].title()
-        underline = "".join(["\n", "-" * len(parts[i]), "\n"])
-        parts.insert(i + 1, underline)
-    return parts
+    if path.is_file():
+        file_size = os.path.getsize(path)
+        return file_size, [str(path)]
+    total = 0
+    filelist = []
+    if path.is_dir():
+        for item in path.iterdir():
+            size, paths = filelist_total(item)
+            total += size
+            filelist.extend(paths)
+    return total, sorted(filelist)
 
+
@@ -13692,62 +19152,231 @@

+ -

+

+filelist_total(pathstring) + + +

+ + +
+ +

Perform error checking and format conversion to os.PathLike.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
pathstring + str +

An existing filesystem path.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ os.PathLike +

Input path converted to bytes format.

+ +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ MissingPathError +

File could not be found.

+
+ Source code in torrentfile\utils.py +
202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
@Memo
+def filelist_total(pathstring: str) -> os.PathLike:
+    """
+    Perform error checking and format conversion to os.PathLike.
+
+    Parameters
+    ----------
+    pathstring : str
+        An existing filesystem path.
+
+    Returns
+    -------
+    os.PathLike
+        Input path converted to bytes format.
+
+    Raises
+    ------
+    MissingPathError
+        File could not be found.
+    """
+    if os.path.exists(pathstring):
+        path = Path(pathstring)
+        return _filelist_total(path)
+    raise MissingPathError
+
+
+

-
-

-activate_logger() +

+get_file_list(path) -

+ +
-

Activate the builtin logging mechanism when passed debug flag from CLI.

+

Return a sorted list of file paths contained in directory.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

target file or directory.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list +

sorted list of file paths.

- Source code in torrentfile\cli.py -
def activate_logger():
+          Source code in torrentfile\utils.py
+          
275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
def get_file_list(path: str) -> list:
     """
-    Activate the builtin logging mechanism when passed debug flag from CLI.
+    Return a sorted list of file paths contained in directory.
+
+    Parameters
+    ----------
+    path : str
+        target file or directory.
+
+    Returns
+    -------
+    list
+        sorted list of file paths.
     """
-    logging.basicConfig(level=logging.INFO)
-    logger = logging.getLogger()
-    file_handler = logging.FileHandler(
-        "torrentfile.log", mode="a+", encoding="utf-8"
-    )
-    console_handler = logging.StreamHandler(stream=sys.stderr)
-    file_formatter = logging.Formatter(
-        "%(asctime)s %(levelno)s %(message)s",
-        datefmt="%m-%d %H:%M:%S",
-        style="%",
-    )
-    stream_formatter = logging.Formatter(
-        "%(asctime)s %(levelno)s %(message)s",
-        datefmt="%m-%d %H:%M:%S",
-        style="%",
-    )
-    file_handler.setFormatter(file_formatter)
-    console_handler.setFormatter(stream_formatter)
-    file_handler.setLevel(logging.INFO)
-    console_handler.setLevel(logging.INFO)
-    logger.setLevel(logging.INFO)
-    logger.addHandler(console_handler)
-    logger.addHandler(file_handler)
-    logger.debug("Debug: ON")
+    _, filelist = filelist_total(path)
+    return filelist
 
+
@@ -13759,384 +19388,314 @@

-

-execute(args=None) +

+get_piece_length(size) -

+ +
-

Initialize Command Line Interface for torrentfile.

+

Calculate the ideal piece length for bittorrent data.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
size + int +

Total bits of all files incluided in .torrent file.

+ required +
+ +

Returns:

+ + - - - - + + - -
argslist

Commandline arguments. default=None

NoneTypeDescription
+ + + + + int + +

Ideal piece length.

+ + + +
- Source code in torrentfile\cli.py -
def execute(args=None):
+          Source code in torrentfile\utils.py
+          
182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
def get_piece_length(size: int) -> int:
     """
-    Initialize Command Line Interface for torrentfile.
+    Calculate the ideal piece length for bittorrent data.
 
     Parameters
     ----------
-    args : list
-        Commandline arguments. default=None
+    size : int
+        Total bits of all files incluided in .torrent file.
+
+    Returns
+    -------
+    int
+        Ideal piece length.
     """
-    if not args:
-        if sys.argv[1:]:
-            args = sys.argv[1:]
-        else:
-            args = ["-h"]
+    exp = 14
+    while size / (2**exp) > 200 and exp < 25:
+        exp += 1
+    return 2**exp
+
+
+
+
- parser = ArgumentParser( - "torrentfile", - description=( - "Command line tools for creating, editing, checking and " - "interacting with Bittorrent metainfo files" - ), - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - conflict_handler="resolve", - ) +
- parser.add_argument( - "-i", - "--interactive", - action="store_true", - dest="interactive", - help="select program options interactively", - ) - parser.add_argument( - "-V", - "--version", - action="version", - version=f"torrentfile v{version}", - help="show program version and exit", - ) - parser.add_argument( - "-v", - "--verbose", - action="store_true", - dest="debug", - help="output debug information", - ) +
- parser.set_defaults(func=parser.print_help) - subparsers = parser.add_subparsers( - title="Actions", - dest="command", - metavar="create, edit, magnet, recheck", - ) - create_parser = subparsers.add_parser( - "create", - help="""Generate a new torrent meta file.""", - prefix_chars="-", - aliases=["c", "new"], - formatter_class=TorrentFileHelpFormatter, - ) +

+humanize_bytes(amount) - create_parser.add_argument( - "-a", - "-t", - "--announce", - "--tracker", - action="store", - dest="announce", - metavar="<url>", - nargs="+", - default=[], - help="One or more space-seperated torrent tracker url(s).", - ) - create_parser.add_argument( - "-p", - "--private", - action="store_true", - dest="private", - help="Creates private torrent with multi-tracker and DHT turned off.", - ) +

- create_parser.add_argument( - "-s", - "--source", - action="store", - dest="source", - metavar="<source>", - help="Add a source string. Useful for cross-seeding.", - ) - create_parser.add_argument( - "-m", - "--magnet", - action="store_true", - dest="magnet", - ) +
- create_parser.add_argument( - "-c", - "--comment", - action="store", - dest="comment", - metavar="<comment>", - help="Include a comment in file metadata", - ) +

Convert integer into human readable memory sized denomination.

- create_parser.add_argument( - "-o", - "--out", - action="store", - dest="outfile", - metavar="<path>", - help="Output save path for created .torrent file", - ) +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
amount + int +

total number of bytes.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

human readable representation of the given amount of bytes.

- create_parser.add_argument( - "--cwd", - "--current", - action="store_true", - dest="cwd", - help="Save output .torrent file to current directory", - ) +
+ Source code in torrentfile\utils.py +
123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
def humanize_bytes(amount: int) -> str:
+    """
+    Convert integer into human readable memory sized denomination.
 
-    create_parser.add_argument(
-        "--prog",
-        "--progress",
-        default="1",
-        action="store",
-        dest="progress",
-        help="""
-        Set the progress bar level.
-        Options = 0, 1
-        (0) = Do not display progress bar.
-        (1) = Display progress bar.
-        """,
-    )
-
-    create_parser.add_argument(
-        "--meta-version",
-        default="1",
-        choices=["1", "2", "3"],
-        action="store",
-        dest="meta_version",
-        metavar="<int>",
-        help="""
-        Bittorrent metafile version.
-        Options = 1, 2, 3
-        (1) = Bittorrent v1 (Default)
-        (2) = Bittorrent v2
-        (3) = Bittorrent v1 & v2 hybrid
-        """,
-    )
-
-    create_parser.add_argument(
-        "--piece-length",
-        action="store",
-        dest="piece_length",
-        metavar="<int>",
-        help="""
-        (Default: <blank>) Number of bytes for per chunk of data transmitted
-        by Bittorrent client. Acceptable values include integers 14-26 which
-        will be interpreted as a perfect power of 2.  e.g. 14 = 16KiB pieces.
-        Examples:: [--piece-length 14] [--piece-length 20]
-        """,
-    )
-
-    create_parser.add_argument(
-        "-w",
-        "--web-seed",
-        action="store",
-        dest="url_list",
-        metavar="<url>",
-        nargs="+",
-        help="list of web addresses where torrent data exists (GetRight).",
-    )
-
-    create_parser.add_argument(
-        "--http-seed",
-        action="store",
-        dest="httpseeds",
-        metavar="<url>",
-        nargs="+",
-        help="list of URLs, addresses where content can be found (Hoffman).",
-    )
-
-    create_parser.add_argument(
-        "content",
-        action="store",
-        metavar="<content>",
-        nargs="?",
-        help="Path to content file or directory",
-    )
-
-    create_parser.set_defaults(func=create)
-
-    edit_parser = subparsers.add_parser(
-        "edit",
-        help="""
-        Edit existing torrent meta file.
-        """,
-        aliases=["e"],
-        prefix_chars="-",
-        formatter_class=TorrentFileHelpFormatter,
-    )
-
-    edit_parser.add_argument(
-        "metafile",
-        action="store",
-        help="path to *.torrent file",
-        metavar="<*.torrent>",
-    )
-
-    edit_parser.add_argument(
-        "--tracker",
-        action="store",
-        dest="announce",
-        metavar="<url>",
-        nargs="+",
-        help="""
-        Replace current list of tracker/announce urls with one or more space
-        seperated Bittorrent tracker announce url(s).
-        """,
-    )
-
-    edit_parser.add_argument(
-        "--web-seed",
-        action="store",
-        dest="url_list",
-        metavar="<url>",
-        nargs="+",
-        help="Replace current list of web-seed urls with one or more url(s)",
-    )
-
-    edit_parser.add_argument(
-        "--http-seed",
-        action="store",
-        dest="httpseeds",
-        metavar="<url>",
-        nargs="+",
-        help="replace all currently listed addresses with new list (Hoffman).",
-    )
-
-    edit_parser.add_argument(
-        "--private",
-        action="store_true",
-        help="Make torrent private.",
-        dest="private",
-    )
+    Parameters
+    ----------
+    amount : int
+        total number of bytes.
 
-    edit_parser.add_argument(
-        "--comment",
-        help="Replaces any existing comment with <comment>",
-        metavar="<comment>",
-        dest="comment",
-        action="store",
-    )
+    Returns
+    -------
+    str
+        human readable representation of the given amount of bytes.
+    """
+    if amount < 1024:
+        return str(amount)
+    if 1024 <= amount < 1_048_576:
+        return f"{amount // 1024} KiB"
+    if 1_048_576 <= amount < 1_073_741_824:
+        return f"{amount // 1_048_576} MiB"
+    return f"{amount // 1073741824} GiB"
+
+
+
+
- edit_parser.add_argument( - "--source", - action="store", - dest="source", - metavar="<source>", - help="Replaces current source with <source>", - ) +
- edit_parser.set_defaults(func=edit) - magnet_parser = subparsers.add_parser( - "magnet", - help=""" - Generate magnet url from an existing Bittorrent meta file. - """, - aliases=["m"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) - magnet_parser.add_argument( - "metafile", - action="store", - help="Path to Bittorrent meta file.", - metavar="<*.torrent>", - ) +
- magnet_parser.set_defaults(func=magnet) - check_parser = subparsers.add_parser( - "recheck", - help=""" - Calculate amount of torrent meta file's content is found on disk. - """, - aliases=["r", "check"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) - check_parser.add_argument( - "metafile", - action="store", - metavar="<*.torrent>", - help="path to .torrent file.", - ) +

+next_power_2(value) - check_parser.add_argument( - "content", - action="store", - metavar="<content>", - help="path to content file or directory", - ) - check_parser.set_defaults(func=recheck) +

- info_parser = subparsers.add_parser( - "info", - help=""" - Show detailed information about a torrent file. - """, - aliases=["i"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) - info_parser.add_argument( - "metafile", - action="store", - metavar="<*.torrent>", - help="path to pre-existing torrent file.", - ) +
- info_parser.set_defaults(func=info) +

Calculate the next perfect power of 2 equal to or greater than value.

- args = parser.parse_args(args) +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
value + int +

integer value that is less than some perfect power of 2.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ int +

The next power of 2 greater than value, or value if already power of 2.

- if args.debug: - activate_logger() +
+ Source code in torrentfile\utils.py +
334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
def next_power_2(value: int) -> int:
+    """
+    Calculate the next perfect power of 2 equal to or greater than value.
 
-    if args.interactive:
-        return select_action()
+    Parameters
+    ----------
+    value : int
+        integer value that is less than some perfect power of 2.
 
-    return args.func(args)
+    Returns
+    -------
+    int
+        The next power of 2 greater than value, or value if already power of 2.
+    """
+    if not value & (value - 1) and value:
+        return value
+    start = 1
+    while start < value:
+        start <<= 1
+    return start
 
+
@@ -14148,24 +19707,148 @@

-

-main() +

+normalize_piece_length(piece_length) -

+ +
-

Initiate main function for CLI script.

+

Verify input piece_length is valid and convert accordingly.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
piece_length + int | str +

The piece length provided by user.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ int +

normalized piece length.

+ +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ PieceLengthValueError : +

If piece length is improper value.

- Source code in torrentfile\cli.py -
def main():
+          Source code in torrentfile\utils.py
+          
146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
def normalize_piece_length(piece_length: int) -> int:
     """
-    Initiate main function for CLI script.
+    Verify input piece_length is valid and convert accordingly.
+
+    Parameters
+    ----------
+    piece_length : int | str
+        The piece length provided by user.
+
+    Returns
+    -------
+    int
+        normalized piece length.
+
+    Raises
+    ------
+    PieceLengthValueError :
+        If piece length is improper value.
     """
-    execute()
+    if isinstance(piece_length, str):
+        if piece_length.isnumeric():
+            piece_length = int(piece_length)
+        else:
+            raise PieceLengthValueError(piece_length)
+
+    if 13 < piece_length < 26:
+        return 2**piece_length
+    if piece_length <= 13:
+        raise PieceLengthValueError(piece_length)
+
+    log = int(math.log2(piece_length))
+    if 2**log == piece_length:
+        return piece_length
+    raise PieceLengthValueError
 
+
@@ -14177,390 +19860,365 @@

-

-main_script(args=None) +

+path_piece_length(path) -

+ +
-

Initialize Command Line Interface for torrentfile.

+

Calculate piece length for input path and contents.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

The absolute path to directory and contents.

+ required +
+ +

Returns:

+ + - - - - + + - -
argslist

Commandline arguments. default=None

NoneTypeDescription
+ + + + + int + +

The size of pieces of torrent content.

+ + + +
- Source code in torrentfile\cli.py -
def execute(args=None):
+          Source code in torrentfile\utils.py
+          
316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
def path_piece_length(path: str) -> int:
     """
-    Initialize Command Line Interface for torrentfile.
+    Calculate piece length for input path and contents.
 
     Parameters
     ----------
-    args : list
-        Commandline arguments. default=None
+    path : str
+        The absolute path to directory and contents.
+
+    Returns
+    -------
+    int
+        The size of pieces of torrent content.
     """
-    if not args:
-        if sys.argv[1:]:
-            args = sys.argv[1:]
-        else:
-            args = ["-h"]
+    psize = path_size(path)
+    return get_piece_length(psize)
+
+
+
+
- parser = ArgumentParser( - "torrentfile", - description=( - "Command line tools for creating, editing, checking and " - "interacting with Bittorrent metainfo files" - ), - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - conflict_handler="resolve", - ) +
- parser.add_argument( - "-i", - "--interactive", - action="store_true", - dest="interactive", - help="select program options interactively", - ) - parser.add_argument( - "-V", - "--version", - action="version", - version=f"torrentfile v{version}", - help="show program version and exit", - ) - parser.add_argument( - "-v", - "--verbose", - action="store_true", - dest="debug", - help="output debug information", - ) +
- parser.set_defaults(func=parser.print_help) - subparsers = parser.add_subparsers( - title="Actions", - dest="command", - metavar="create, edit, magnet, recheck", - ) - create_parser = subparsers.add_parser( - "create", - help="""Generate a new torrent meta file.""", - prefix_chars="-", - aliases=["c", "new"], - formatter_class=TorrentFileHelpFormatter, - ) +

+path_size(path) - create_parser.add_argument( - "-a", - "-t", - "--announce", - "--tracker", - action="store", - dest="announce", - metavar="<url>", - nargs="+", - default=[], - help="One or more space-seperated torrent tracker url(s).", - ) - create_parser.add_argument( - "-p", - "--private", - action="store_true", - dest="private", - help="Creates private torrent with multi-tracker and DHT turned off.", - ) +

- create_parser.add_argument( - "-s", - "--source", - action="store", - dest="source", - metavar="<source>", - help="Add a source string. Useful for cross-seeding.", - ) - create_parser.add_argument( - "-m", - "--magnet", - action="store_true", - dest="magnet", - ) +
- create_parser.add_argument( - "-c", - "--comment", - action="store", - dest="comment", - metavar="<comment>", - help="Include a comment in file metadata", - ) +

Return the total size of all files in path recursively.

- create_parser.add_argument( - "-o", - "--out", - action="store", - dest="outfile", - metavar="<path>", - help="Output save path for created .torrent file", - ) +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

path to target file or directory.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ int +

total size of files.

- create_parser.add_argument( - "--cwd", - "--current", - action="store_true", - dest="cwd", - help="Save output .torrent file to current directory", - ) +
+ Source code in torrentfile\utils.py +
257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
def path_size(path: str) -> int:
+    """
+    Return the total size of all files in path recursively.
 
-    create_parser.add_argument(
-        "--prog",
-        "--progress",
-        default="1",
-        action="store",
-        dest="progress",
-        help="""
-        Set the progress bar level.
-        Options = 0, 1
-        (0) = Do not display progress bar.
-        (1) = Display progress bar.
-        """,
-    )
+    Parameters
+    ----------
+    path : str
+        path to target file or directory.
 
-    create_parser.add_argument(
-        "--meta-version",
-        default="1",
-        choices=["1", "2", "3"],
-        action="store",
-        dest="meta_version",
-        metavar="<int>",
-        help="""
-        Bittorrent metafile version.
-        Options = 1, 2, 3
-        (1) = Bittorrent v1 (Default)
-        (2) = Bittorrent v2
-        (3) = Bittorrent v1 & v2 hybrid
-        """,
-    )
+    Returns
+    -------
+    int
+        total size of files.
+    """
+    total_size, _ = filelist_total(path)
+    return total_size
+
+
+
+
- create_parser.add_argument( - "--piece-length", - action="store", - dest="piece_length", - metavar="<int>", - help=""" - (Default: <blank>) Number of bytes for per chunk of data transmitted - by Bittorrent client. Acceptable values include integers 14-26 which - will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. - Examples:: [--piece-length 14] [--piece-length 20] - """, - ) +
- create_parser.add_argument( - "-w", - "--web-seed", - action="store", - dest="url_list", - metavar="<url>", - nargs="+", - help="list of web addresses where torrent data exists (GetRight).", - ) - create_parser.add_argument( - "--http-seed", - action="store", - dest="httpseeds", - metavar="<url>", - nargs="+", - help="list of URLs, addresses where content can be found (Hoffman).", - ) - create_parser.add_argument( - "content", - action="store", - metavar="<content>", - nargs="?", - help="Path to content file or directory", - ) +
- create_parser.set_defaults(func=create) - edit_parser = subparsers.add_parser( - "edit", - help=""" - Edit existing torrent meta file. - """, - aliases=["e"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) - edit_parser.add_argument( - "metafile", - action="store", - help="path to *.torrent file", - metavar="<*.torrent>", - ) +

+path_stat(path) - edit_parser.add_argument( - "--tracker", - action="store", - dest="announce", - metavar="<url>", - nargs="+", - help=""" - Replace current list of tracker/announce urls with one or more space - seperated Bittorrent tracker announce url(s). - """, - ) - edit_parser.add_argument( - "--web-seed", - action="store", - dest="url_list", - metavar="<url>", - nargs="+", - help="Replace current list of web-seed urls with one or more url(s)", - ) +

- edit_parser.add_argument( - "--http-seed", - action="store", - dest="httpseeds", - metavar="<url>", - nargs="+", - help="replace all currently listed addresses with new list (Hoffman).", - ) - edit_parser.add_argument( - "--private", - action="store_true", - help="Make torrent private.", - dest="private", - ) +
- edit_parser.add_argument( - "--comment", - help="Replaces any existing comment with <comment>", - metavar="<comment>", - dest="comment", - action="store", - ) +

Calculate directory statistics.

- edit_parser.add_argument( - "--source", - action="store", - dest="source", - metavar="<source>", - help="Replaces current source with <source>", - ) +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

The path to start calculating from.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + + + + + + + + + +
TypeDescription
+ list +

List of all files contained in Directory

+ int +

Total sum of bytes from all contents of dir

+ int +

The size of pieces of the torrent contents.

- edit_parser.set_defaults(func=edit) +
+ Source code in torrentfile\utils.py +
293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
def path_stat(path: str) -> tuple:
+    """
+    Calculate directory statistics.
 
-    magnet_parser = subparsers.add_parser(
-        "magnet",
-        help="""
-        Generate magnet url from an existing Bittorrent meta file.
-        """,
-        aliases=["m"],
-        prefix_chars="-",
-        formatter_class=TorrentFileHelpFormatter,
-    )
+    Parameters
+    ----------
+    path : str
+        The path to start calculating from.
 
-    magnet_parser.add_argument(
-        "metafile",
-        action="store",
-        help="Path to Bittorrent meta file.",
-        metavar="<*.torrent>",
-    )
+    Returns
+    -------
+    list
+        List of all files contained in Directory
+    int
+        Total sum of bytes from all contents of dir
+    int
+        The size of pieces of the torrent contents.
+    """
+    total_size, filelist = filelist_total(path)
+    piece_length = get_piece_length(total_size)
+    return (filelist, total_size, piece_length)
+
+
+
+
- magnet_parser.set_defaults(func=magnet) +
- check_parser = subparsers.add_parser( - "recheck", - help=""" - Calculate amount of torrent meta file's content is found on disk. - """, - aliases=["r", "check"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) - check_parser.add_argument( - "metafile", - action="store", - metavar="<*.torrent>", - help="path to .torrent file.", - ) - check_parser.add_argument( - "content", - action="store", - metavar="<content>", - help="path to content file or directory", - ) - check_parser.set_defaults(func=recheck) - info_parser = subparsers.add_parser( - "info", - help=""" - Show detailed information about a torrent file. - """, - aliases=["i"], - prefix_chars="-", - formatter_class=TorrentFileHelpFormatter, - ) +
+ +
+ +
+ + + +
+ + + +

+ version + + + +

+ +
+ +

Holds the release version number.

+ + + +
+ + + + - info_parser.add_argument( - "metafile", - action="store", - metavar="<*.torrent>", - help="path to pre-existing torrent file.", - ) - info_parser.set_defaults(func=info) - args = parser.parse_args(args) - if args.debug: - activate_logger() - if args.interactive: - return select_action() - return args.func(args) -
- -
+
+
@@ -14577,8 +20235,8 @@

-

- torrentfile.edit +

+ torrentfile.cli @@ -14586,19 +20244,13 @@

-

Edit torrent module.

-

Provides a facility by which certain properties of a torrent meta file can be -edited by the user. The various command line arguments indicate which fields -should be edited, and what the new value should be. Depending on what fields -are chosen to edit, this command can trigger a new info hash which means the -torrent will no longer be able to participate in the same swarm as the original -unedited torrent.

-

Keywords

-

private -comment -source -trackers -web-seeds

+

Command Line Interface for TorrentFile project.

+

This module provides the primary command line argument parser for +the torrentfile package. The main_script function is automatically +invoked when called from command line, and parses accompanying arguments.

+

Functions: + main_script: process command line arguments and run program. + activate_logger: turns on debug mode and logging facility.

@@ -14611,122 +20263,329 @@

Keywords

+
-
+

+ TorrentFileHelpFormatter -

-edit_torrent(metafile, args)

+
+

+ Bases: HelpFormatter

-

Edit the properties and values in a torrent meta file.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
metafilestr

path to the torrent meta file.

required
argsdict

key value pairs of the properties to be edited.

required
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
dict

The edited and nested Meta and info dictionaries.

-
- Source code in torrentfile\edit.py -
def edit_torrent(metafile: str, args: dict) -> dict:
-    """
-    Edit the properties and values in a torrent meta file.
+      

Formatting class for help tips provided by the CLI.

+

Subclasses Argparse.HelpFormatter.

- Parameters - ---------- - metafile : str - path to the torrent meta file. - args : dict - key value pairs of the properties to be edited. - Returns - ------- - dict - The edited and nested Meta and info dictionaries. +
+ Source code in torrentfile\cli.py +
 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
class TorrentFileHelpFormatter(HelpFormatter):
+    """
+    Formatting class for help tips provided by the CLI.
+
+    Subclasses Argparse.HelpFormatter.
     """
-    logger.debug("editing torrent file %s", metafile)
-    meta = pyben.load(metafile)
-    info = meta["info"]
-    filter_empty(args, meta, info)
 
-    if "comment" in args:
-        info["comment"] = args["comment"]
+    def __init__(self, prog, width=40, max_help_positions=30):
+        """
+        Construct HelpFormat class for usage output.
 
-    if "source" in args:
-        info["source"] = args["source"]
+        Parameters
+        ----------
+        prog : str
+            Name of the program.
+        width : int
+            Max width of help message output.
+        max_help_positions : int
+            max length until line wrap.
+        """
+        super().__init__(
+            prog, width=width, max_help_position=max_help_positions
+        )
 
-    if "private" in args:
-        info["private"] = 1
+    def _split_lines(self, text, _):
+        """
+        Split multiline help messages and remove indentation.
 
-    if "announce" in args:
-        val = args.get("announce", None)
-        if isinstance(val, str):
-            vallist = val.split()
-            meta["announce"] = vallist[0]
-            meta["announce-list"] = [vallist]
-        elif isinstance(val, list):
-            meta["announce"] = val[0]
-            meta["announce-list"] = [val]
+        Parameters
+        ----------
+        text : str
+            text that needs to be split
+        _ : int
+            max width for line.
+        """
+        lines = text.split("\n")
+        return [line.strip() for line in lines if line]
 
-    if "url-list" in args:
-        val = args.get("url-list")
-        if isinstance(val, str):
-            meta["url-list"] = val.split()
-        elif isinstance(val, list):
-            meta["url-list"] = val
+    def _format_text(self, text):
+        """
+        Format text for cli usage messages.
 
-    if "httpseeds" in args:
-        val = args.get("httpseeds")
-        if isinstance(val, str):
-            meta["httpseeds"] = val.split()
-        elif isinstance(val, list):
-            meta["httpseeds"] = val
+        Parameters
+        ----------
+        text : str
+            Pre-formatted text.
 
-    meta["info"] = info
-    os.remove(metafile)
-    pyben.dump(meta, metafile)
-    return meta
+        Returns
+        -------
+        str
+            Formatted text from input.
+        """
+        text = text % dict(prog=self._prog) if "%(prog)" in text else text
+        text = self._whitespace_matcher.sub(" ", text).strip()
+        return text + "\n\n"
+
+    def _join_parts(self, part_strings):
+        """
+        Combine different sections of the help message.
+
+        Parameters
+        ----------
+        part_strings : list
+            List of argument help messages and headers.
+
+        Returns
+        -------
+        str
+            Fully formatted help message for CLI.
+        """
+        parts = self.format_headers(part_strings)
+        return super()._join_parts(parts)
+
+    @staticmethod
+    def format_headers(parts):
+        """
+        Format help message section headers.
+
+        Parameters
+        ----------
+        parts : list
+            List of individual lines for help message.
+
+        Returns
+        -------
+        list
+            Input list with formatted section headers.
+        """
+        if parts and parts[0].startswith("usage:"):
+            parts[0] = "Usage\n=====\n  " + parts[0][6:]
+        headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
+        for i in headings[::-1]:
+            parts[i] = parts[i][:-2].title()
+            underline = "".join(["\n", "-" * len(parts[i]), "\n"])
+            parts.insert(i + 1, underline)
+        return parts
+
+
+
+ + + +
+ + + + + + + + +
+ + + +

+__init__(prog, width=40, max_help_positions=30) + + +

+ + +
+ +

Construct HelpFormat class for usage output.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
prog + str +

Name of the program.

+ required +
width + int +

Max width of help message output.

+ 40 +
max_help_positions + int +

max length until line wrap.

+ 30 +
+ +
+ Source code in torrentfile\cli.py +
77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
def __init__(self, prog, width=40, max_help_positions=30):
+    """
+    Construct HelpFormat class for usage output.
+
+    Parameters
+    ----------
+    prog : str
+        Name of the program.
+    width : int
+        Max width of help message output.
+    max_help_positions : int
+        max length until line wrap.
+    """
+    super().__init__(
+        prog, width=width, max_help_position=max_help_positions
+    )
 
+
@@ -14738,75 +20597,96 @@

-

-filter_empty(args, meta, info) +

+_format_text(text) -

+

+
-

Remove dictionary keys with empty values.

+

Format text for cli usage messages.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
argsdict

Editable metafile properties from user.

required
+ - - - - + + + + + + + + + + + + + +
metadict

Metafile data dictionary.

requiredNameTypeDescriptionDefault
text + str +

Pre-formatted text.

+ required +
+ +

Returns:

+ + - - - - + + - -
infodict

Metafile info dictionary.

requiredTypeDescription
+ + + + + str + +

Formatted text from input.

+ + + +
- Source code in torrentfile\edit.py -
def filter_empty(args: dict, meta: dict, info: dict):
-    """
-    Remove dictionary keys with empty values.
+          Source code in torrentfile\cli.py
+          
108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
def _format_text(self, text):
+    """
+    Format text for cli usage messages.
 
     Parameters
     ----------
-    args : dict
-        Editable metafile properties from user.
-    meta : dict
-        Metafile data dictionary.
-    info : dict
-        Metafile info dictionary.
-    """
-    for key, val in list(args.items()):
-        if val is None:
-            del args[key]
-            continue
+    text : str
+        Pre-formatted text.
 
-        if val == "":
-            if key in meta:
-                del meta[key]
-            elif key in info:
-                del info[key]
-            del args[key]
-            logger.debug("removeing empty fields %s", val)
+    Returns
+    -------
+    str
+        Formatted text from input.
+    """
+    text = text % dict(prog=self._prog) if "%(prog)" in text else text
+    text = self._whitespace_matcher.sub(" ", text).strip()
+    return text + "\n\n"
 
+
@@ -14814,294 +20694,391 @@

+
-
+

+_join_parts(part_strings) - - +

+
-
+

Combine different sections of the help message.

+

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
part_strings + list +

List of argument help messages and headers.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

Fully formatted help message for CLI.

+
+ Source code in torrentfile\cli.py +
126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
def _join_parts(self, part_strings):
+    """
+    Combine different sections of the help message.
 
-

- torrentfile.hasher + Parameters + ---------- + part_strings : list + List of argument help messages and headers. + Returns + ------- + str + Fully formatted help message for CLI. + """ + parts = self.format_headers(part_strings) + return super()._join_parts(parts) +

+
+
+
+
- -
-

Piece/File Hashers for Bittorrent meta file contents.

+
-
+

+_split_lines(text, _) +

+
+

Split multiline help messages and remove indentation.

+

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
text + str +

text that needs to be split

+ required +
_ + int +

max width for line.

+ required +
+
+ Source code in torrentfile\cli.py +
 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
def _split_lines(self, text, _):
+    """
+    Split multiline help messages and remove indentation.
 
+    Parameters
+    ----------
+    text : str
+        text that needs to be split
+    _ : int
+        max width for line.
+    """
+    lines = text.split("\n")
+    return [line.strip() for line in lines if line]
+
+
+
+
+
-
+
-

- -FileHasher (CbMixin, ProgMixin) - +

+format_headers(parts) + + + staticmethod + + +

-
-

Calculate root and piece hashes for creating hybrid torrent file.

-

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. -With a branching factor of 2, merge layer hashes until blocks equal -piece_length bytes for the piece layer, and then the root hash.

+

Format help message section headers.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
pathstr

path to target file.

required
+ - - - - + + + + + + + + + + + + + +
piece_lengthint

piece length for data chunks.

requiredNameTypeDescriptionDefault
parts + list +

List of individual lines for help message.

+ required +
+ +

Returns:

+ + - - - - + + - -
progressbool

default = None

TrueTypeDescription
+ + + + + list + +

Input list with formatted section headers.

+ + + +
- Source code in torrentfile\hasher.py -
class FileHasher(CbMixin, ProgMixin):
+          Source code in torrentfile\cli.py
+          
143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
@staticmethod
+def format_headers(parts):
     """
-    Calculate root and piece hashes for creating hybrid torrent file.
-
-    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
-    With a branching factor of 2, merge layer hashes until blocks equal
-    piece_length bytes for the piece layer, and then the root hash.
+    Format help message section headers.
 
     Parameters
     ----------
-    path : str
-        path to target file.
-    piece_length : int
-        piece length for data chunks.
-    progress : int
-        default = None
-    """
-
-    def __init__(
-        self,
-        path: str,
-        piece_length: int,
-        progress: bool = True,
-        hybrid: bool = False,
-    ):
-        """
-        Construct Hasher class instances for each file in torrent.
-        """
-        self.path = path
-        self.piece_length = piece_length
-        self.pieces = []
-        self.layer_hashes = []
-        self.piece_layer = None
-        self.root = None
-        self.padding_piece = None
-        self.padding_file = None
-        self.amount = piece_length // BLOCK_SIZE
-        self.end = False
-        self.current = open(path, "rb")
-        self.hybrid = hybrid
-        if progress:
-            self.progressbar = True
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
-
-    def __iter__(self):
-        """Return `self`: needed to implement iterator implementation."""
-        return self
-
-    def _pad_remaining(self, block_count: int):
-        """
-        Generate Hash sized, 0 filled bytes for padding.
-
-        Parameters
-        ----------
-        block_count : int
-            current total number of blocks collected.
-
-        Returns
-        -------
-        padding : bytes
-            Padding to fill remaining portion of tree.
-        """
-        # when the there is only one block for file
-        remaining = self.amount - block_count
-        if not self.layer_hashes:
-            power2 = next_power_2(block_count)
-            remaining = power2 - block_count
-        self.prog_update(HASH_SIZE * remaining)
-        return [bytes(HASH_SIZE) for _ in range(remaining)]
-
-    def __next__(self):
-        """
-        Calculate layer hashes for contents of file.
-
-        Parameters
-        ----------
-        data : BytesIO
-            File opened in read mode.
-        """
-        if self.end:
-            self.end = False
-            raise StopIteration
-        plength = self.piece_length
-        blocks = []
-        piece = sha1()  # nosec
-        total = 0
-        block = bytearray(BLOCK_SIZE)
-        for _ in range(self.amount):
-            size = self.current.readinto(block)
-            self.prog_update(size)
-            if not size:
-                self.end = True
-                break
-            total += size
-            plength -= size
-            blocks.append(sha256(block[:size]).digest())
-            if self.hybrid:
-                piece.update(block[:size])
-        if len(blocks) != self.amount:
-            padding = self._pad_remaining(len(blocks))
-            blocks.extend(padding)
-        layer_hash = merkle_root(blocks)
-        self.layer_hashes.append(layer_hash)
-        if self._cb:
-            self._cb(layer_hash)
-        if self.end:
-            self._calculate_root()
-            self.prog_close()
-        if self.hybrid:
-            if plength > 0:
-                self.padding_file = {
-                    "attr": "p",
-                    "length": plength,
-                    "path": [".pad", str(plength)],
-                }
-                piece.update(bytes(plength))
-            piece = piece.digest()
-            self.pieces.append(piece)
-            return layer_hash, piece
-        return layer_hash
-
-    def _calculate_root(self):
-        """
-        Calculate the root hash for opened file.
-        """
-        self.piece_layer = b"".join(self.layer_hashes)
-
-        if len(self.layer_hashes) > 1:
-            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
-
-            pow2 = next_power_2(len(self.layer_hashes))
-            remainder = pow2 - len(self.layer_hashes)
+    parts : list
+        List of individual lines for help message.
 
-            self.layer_hashes += [pad_piece for _ in range(remainder)]
-        self.root = merkle_root(self.layer_hashes)
-        self.current.close()
+    Returns
+    -------
+    list
+        Input list with formatted section headers.
+    """
+    if parts and parts[0].startswith("usage:"):
+        parts[0] = "Usage\n=====\n  " + parts[0][6:]
+    headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
+    for i in headings[::-1]:
+        parts[i] = parts[i][:-2].title()
+        underline = "".join(["\n", "-" * len(parts[i]), "\n"])
+        parts.insert(i + 1, underline)
+    return parts
 
+
+
+
-
+
+
+
-
+
-

-__init__(self, path, piece_length, progress=True, hybrid=False) +

+activate_logger() - - special - -

+ +
-

Construct Hasher class instances for each file in torrent.

+

Activate the builtin logging mechanism when passed debug flag from CLI.

- Source code in torrentfile\hasher.py -
def __init__(
-    self,
-    path: str,
-    piece_length: int,
-    progress: bool = True,
-    hybrid: bool = False,
-):
+          Source code in torrentfile\cli.py
+          
40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
def activate_logger():
     """
-    Construct Hasher class instances for each file in torrent.
+    Activate the builtin logging mechanism when passed debug flag from CLI.
     """
-    self.path = path
-    self.piece_length = piece_length
-    self.pieces = []
-    self.layer_hashes = []
-    self.piece_layer = None
-    self.root = None
-    self.padding_piece = None
-    self.padding_file = None
-    self.amount = piece_length // BLOCK_SIZE
-    self.end = False
-    self.current = open(path, "rb")
-    self.hybrid = hybrid
-    if progress:
-        self.progressbar = True
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
+    logging.basicConfig(level=logging.INFO)
+    logger = logging.getLogger()
+    # file_handler = logging.FileHandler(
+    #     "torrentfile.log", mode="a+", encoding="utf-8"
+    # )
+    console_handler = logging.StreamHandler(stream=sys.stderr)
+    # file_formatter = logging.Formatter(
+    #     "%(asctime)s %(levelno)s %(message)s",
+    #     datefmt="%m-%d %H:%M:%S",
+    #     style="%",
+    # )
+    stream_formatter = logging.Formatter(
+        "%(asctime)s %(levelno)s %(message)s",
+        datefmt="%m-%d %H:%M:%S",
+        style="%",
+    )
+    # file_handler.setFormatter(file_formatter)
+    console_handler.setFormatter(stream_formatter)
+    # file_handler.setLevel(logging.INFO)
+    console_handler.setLevel(logging.DEBUG)
+    logger.setLevel(logging.DEBUG)
+    logger.addHandler(console_handler)
+    # logger.addHandler(file_handler)
+    logger.debug("Debug: ON")
 
+
@@ -15109,402 +21086,1049 @@

-
+
-

-__iter__(self) +

+execute(args=None) - - special - -

+

+
-

Return self: needed to implement iterator implementation.

+

Initialize Command Line Interface for torrentfile.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + list +

Commandline arguments. default=None

+ None +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ list +

Depends on what the command line args were.

- Source code in torrentfile\hasher.py -
def __iter__(self):
-    """Return `self`: needed to implement iterator implementation."""
-    return self
-
-
-
+ Source code in torrentfile\cli.py +
168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
def execute(args=None) -> list:
+    """
+    Initialize Command Line Interface for torrentfile.
 
-  
+ Parameters + ---------- + args : list + Commandline arguments. default=None + Returns + ------- + list + Depends on what the command line args were. + """ + if not args: + if sys.argv[1:]: + args = sys.argv[1:] + else: + args = ["-h"] + parser = ArgumentParser( + "torrentfile", + description=( + "Command line tools for creating, editing, checking and " + "interacting with Bittorrent metainfo files" + ), + prefix_chars="-", + formatter_class=TorrentFileHelpFormatter, + conflict_handler="resolve", + ) -
+ parser.add_argument( + "-i", + "--interactive", + action="store_true", + dest="interactive", + help="select program options interactively", + ) + parser.add_argument( + "-V", + "--version", + action="version", + version=f"torrentfile v{version}", + help="show program version and exit", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + dest="debug", + help="output debug information", + ) -

-__next__(self) + parser.set_defaults(func=parser.print_help) - - special - + subparsers = parser.add_subparsers( + title="Actions", + dest="command", + metavar="create, edit, magnet, recheck", + ) -

+ create_parser = subparsers.add_parser( + "create", + help="""Generate a new torrent meta file.""", + prefix_chars="-", + aliases=["c", "new"], + formatter_class=TorrentFileHelpFormatter, + ) -
+ create_parser.add_argument( + "-a", + "-t", + "--announce", + "--tracker", + action="store", + dest="announce", + metavar="<url>", + nargs="+", + default=[], + help="One or more space-seperated torrent tracker url(s).", + ) -

Calculate layer hashes for contents of file.

+ create_parser.add_argument( + "-p", + "--private", + action="store_true", + dest="private", + help="Creates private torrent with multi-tracker and DHT turned off.", + ) -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
dataBytesIO

File opened in read mode.

required
-
- Source code in torrentfile\hasher.py -
def __next__(self):
-    """
-    Calculate layer hashes for contents of file.
+    create_parser.add_argument(
+        "-s",
+        "--source",
+        action="store",
+        dest="source",
+        metavar="<source>",
+        help="Add a source string. Useful for cross-seeding.",
+    )
 
-    Parameters
-    ----------
-    data : BytesIO
-        File opened in read mode.
-    """
-    if self.end:
-        self.end = False
-        raise StopIteration
-    plength = self.piece_length
-    blocks = []
-    piece = sha1()  # nosec
-    total = 0
-    block = bytearray(BLOCK_SIZE)
-    for _ in range(self.amount):
-        size = self.current.readinto(block)
-        self.prog_update(size)
-        if not size:
-            self.end = True
-            break
-        total += size
-        plength -= size
-        blocks.append(sha256(block[:size]).digest())
-        if self.hybrid:
-            piece.update(block[:size])
-    if len(blocks) != self.amount:
-        padding = self._pad_remaining(len(blocks))
-        blocks.extend(padding)
-    layer_hash = merkle_root(blocks)
-    self.layer_hashes.append(layer_hash)
-    if self._cb:
-        self._cb(layer_hash)
-    if self.end:
-        self._calculate_root()
-        self.prog_close()
-    if self.hybrid:
-        if plength > 0:
-            self.padding_file = {
-                "attr": "p",
-                "length": plength,
-                "path": [".pad", str(plength)],
-            }
-            piece.update(bytes(plength))
-        piece = piece.digest()
-        self.pieces.append(piece)
-        return layer_hash, piece
-    return layer_hash
-
-
-
+ create_parser.add_argument( + "-m", + "--magnet", + action="store_true", + dest="magnet", + ) + + create_parser.add_argument( + "-c", + "--comment", + action="store", + dest="comment", + metavar="<comment>", + help="Include a comment in file metadata", + ) + + create_parser.add_argument( + "-o", + "--out", + action="store", + dest="outfile", + metavar="<path>", + help="Output save path for created .torrent file", + ) + + create_parser.add_argument( + "--cwd", + "--current", + action="store_true", + dest="cwd", + help="Save output .torrent file to current directory", + ) + + create_parser.add_argument( + "--prog", + "--progress", + default="1", + action="store", + dest="progress", + help=""" + Set the progress bar level. + Options = 0, 1 + (0) = Do not display progress bar. + (1) = Display progress bar. + """, + ) + + create_parser.add_argument( + "--meta-version", + default="1", + choices=["1", "2", "3"], + action="store", + dest="meta_version", + metavar="<int>", + help=""" + Bittorrent metafile version. + Options = 1, 2, 3 + (1) = Bittorrent v1 (Default) + (2) = Bittorrent v2 + (3) = Bittorrent v1 & v2 hybrid + """, + ) + + create_parser.add_argument( + "--piece-length", + action="store", + dest="piece_length", + metavar="<int>", + help=""" + (Default: <blank>) Number of bytes for per chunk of data transmitted + by Bittorrent client. Acceptable values include integers 14-26 which + will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. + Examples:: [--piece-length 14] [--piece-length 20] + """, + ) + + create_parser.add_argument( + "-w", + "--web-seed", + action="store", + dest="url_list", + metavar="<url>", + nargs="+", + help="list of web addresses where torrent data exists (GetRight).", + ) + + create_parser.add_argument( + "--http-seed", + action="store", + dest="httpseeds", + metavar="<url>", + nargs="+", + help="list of URLs, addresses where content can be found (Hoffman).", + ) + + create_parser.add_argument( + "content", + action="store", + metavar="<content>", + nargs="?", + help="Path to content file or directory", + ) -
+ create_parser.set_defaults(func=create) + edit_parser = subparsers.add_parser( + "edit", + help=""" + Edit existing torrent meta file. + """, + aliases=["e"], + prefix_chars="-", + formatter_class=TorrentFileHelpFormatter, + ) + edit_parser.add_argument( + "metafile", + action="store", + help="path to *.torrent file", + metavar="<*.torrent>", + ) + edit_parser.add_argument( + "--tracker", + action="store", + dest="announce", + metavar="<url>", + nargs="+", + help=""" + Replace current list of tracker/announce urls with one or more space + seperated Bittorrent tracker announce url(s). + """, + ) + edit_parser.add_argument( + "--web-seed", + action="store", + dest="url_list", + metavar="<url>", + nargs="+", + help="Replace current list of web-seed urls with one or more url(s)", + ) - + edit_parser.add_argument( + "--http-seed", + action="store", + dest="httpseeds", + metavar="<url>", + nargs="+", + help="replace all currently listed addresses with new list (Hoffman).", + ) - + edit_parser.add_argument( + "--private", + action="store_true", + help="Make torrent private.", + dest="private", + ) - + edit_parser.add_argument( + "--comment", + help="Replaces any existing comment with <comment>", + metavar="<comment>", + dest="comment", + action="store", + ) + edit_parser.add_argument( + "--source", + action="store", + dest="source", + metavar="<source>", + help="Replaces current source with <source>", + ) + edit_parser.set_defaults(func=edit) -
+ magnet_parser = subparsers.add_parser( + "magnet", + help=""" + Generate magnet url from an existing Bittorrent meta file. + """, + aliases=["m"], + prefix_chars="-", + formatter_class=TorrentFileHelpFormatter, + ) + magnet_parser.add_argument( + "metafile", + action="store", + help="Path to Bittorrent meta file.", + metavar="<*.torrent>", + ) + magnet_parser.set_defaults(func=magnet) -

- -Hasher (CbMixin, ProgMixin) - + check_parser = subparsers.add_parser( + "recheck", + help=""" + Calculate amount of torrent meta file's content is found on disk. + """, + aliases=["r", "check"], + prefix_chars="-", + formatter_class=TorrentFileHelpFormatter, + ) + check_parser.add_argument( + "metafile", + action="store", + metavar="<*.torrent>", + help="path to .torrent file.", + ) + check_parser.add_argument( + "content", + action="store", + metavar="<content>", + help="path to content file or directory", + ) -

+ check_parser.set_defaults(func=recheck) -
+ info_parser = subparsers.add_parser( + "info", + help=""" + Show detailed information about a torrent file. + """, + aliases=["i"], + prefix_chars="-", + formatter_class=TorrentFileHelpFormatter, + ) -

Piece hasher for Bittorrent V1 files.

-

Takes a sorted list of all file paths, calculates sha1 hash -for fixed size pieces of file data from each file -seemlessly until the last piece which may be smaller than others.

+ info_parser.add_argument( + "metafile", + action="store", + metavar="<*.torrent>", + help="path to pre-existing torrent file.", + ) -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
pathslist

List of files.

required
piece_lengthint

Size of chuncks to split the data into.

required
progressbool

default = None

True
-
- Source code in torrentfile\hasher.py -
class Hasher(CbMixin, ProgMixin):
-    """
-    Piece hasher for Bittorrent V1 files.
+    info_parser.set_defaults(func=info)
 
-    Takes a sorted list of all file paths, calculates sha1 hash
-    for fixed size pieces of file data from each file
-    seemlessly until the last piece which may be smaller than others.
+    args = parser.parse_args(args)
 
-    Parameters
-    ----------
-    paths : list
-        List of files.
-    piece_length : int
-        Size of chuncks to split the data into.
-    progress : int
-        default = None
-    """
+    if args.debug:
+        activate_logger()
 
-    def __init__(self, paths: list, piece_length: int, progress: bool = True):
-        """Generate hashes of piece length data from filelist contents."""
-        self.piece_length = piece_length
-        self.paths = paths
-        self.progress = progress
-        self.total = sum([os.path.getsize(i) for i in self.paths])
-        self.index = 0
-        self.current = open(self.paths[0], "rb")
-        if self.progress:
-            total = os.path.getsize(self.paths[0])
-            self.prog_start(total, self.paths[0], unit="bytes")
-        logger.debug("Hashing %s", str(self.paths[0]))
+    if args.interactive:
+        return select_action()
 
-    def __iter__(self):
-        """
-        Iterate through feed pieces.
+    if hasattr(args, "func"):
+        return args.func(args)
+    return args
+
+
+ +
- Returns - ------- - self : iterator - Iterator for leaves/hash pieces. - """ - return self +
- def _handle_partial(self, arr: bytearray) -> bytearray: - """ - Define the handling partial pieces that span 2 or more files. - Parameters - ---------- - arr : bytearray - Incomplete piece containing partial data - Returns - ------- - digest : bytearray - SHA1 digest of the complete piece. - """ - while len(arr) < self.piece_length and self.next_file(): - target = self.piece_length - len(arr) - temp = bytearray(target) - size = self.current.readinto(temp) - arr.extend(temp[:size]) - self.prog_update(size) - if size == target: - break - return sha1(arr).digest() # nosec +
- def next_file(self) -> bool: - """ - Seemlessly transition to next file in file list. - Returns - ------- - bool: - True if there is a next file otherwise False. - """ - self.index += 1 - self.prog_close() - if self.index < len(self.paths): - path = self.paths[self.index] - logger.debug("Hashing %s", str(path)) - self.current.close() - if self.progress: - self.prog_start(os.path.getsize(path), path, unit="bytes") - self.current = open(path, "rb") - return True - return False - def __next__(self) -> bytes: - """ - Generate piece-length pieces of data from input file list. +

+main() - Returns - ------- - bytes - SHA1 hash of the piece extracted. - """ - while True: - piece = bytearray(self.piece_length) - size = self.current.readinto(piece) - if size == 0: - if not self.next_file(): - raise StopIteration - elif size < self.piece_length: - self.prog_update(size) - return self._handle_partial(piece[:size]) - else: - self.prog_update(size) - return sha1(piece).digest() # nosec + +

+ + +
+ +

Initiate main function for CLI script.

+ +
+ Source code in torrentfile\cli.py +
526
+527
+528
+529
+530
def main():
+    """
+    Initiate main function for CLI script.
+    """
+    execute()
 
+
+
+
-
+
+
+ +
-
+

+ torrentfile.edit -

-__init__(self, paths, piece_length, progress=True) - - special - -

+ -
+
-

Generate hashes of piece length data from filelist contents.

+

Edit torrent module.

+

Provides a facility by which certain properties of a torrent meta file can be +edited by the user. The various command line arguments indicate which fields +should be edited, and what the new value should be. Depending on what fields +are chosen to edit, this command can trigger a new info hash which means the +torrent will no longer be able to participate in the same swarm as the original +unedited torrent.

+

Keywords

+

private +comment +source +trackers +web-seeds

-
- Source code in torrentfile\hasher.py -
def __init__(self, paths: list, piece_length: int, progress: bool = True):
-    """Generate hashes of piece length data from filelist contents."""
-    self.piece_length = piece_length
-    self.paths = paths
-    self.progress = progress
-    self.total = sum([os.path.getsize(i) for i in self.paths])
-    self.index = 0
-    self.current = open(self.paths[0], "rb")
-    if self.progress:
-        total = os.path.getsize(self.paths[0])
-        self.prog_start(total, self.paths[0], unit="bytes")
-    logger.debug("Hashing %s", str(self.paths[0]))
-
-
-
-
+
-
-

-__iter__(self) - - special - -

+ + +
+ + + +

+edit_torrent(metafile, args) + + +

+
-

Iterate through feed pieces.

+

Edit the properties and values in a torrent meta file.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
metafile + str +

path to the torrent meta file.

+ required +
args + dict +

key value pairs of the properties to be edited.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ dict +

The edited and nested Meta and info dictionaries.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
iterator

Iterator for leaves/hash pieces.

- Source code in torrentfile\hasher.py -
def __iter__(self):
+          Source code in torrentfile\edit.py
+          
 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
def edit_torrent(metafile: str, args: dict) -> dict:
     """
-    Iterate through feed pieces.
+    Edit the properties and values in a torrent meta file.
+
+    Parameters
+    ----------
+    metafile : str
+        path to the torrent meta file.
+    args : dict
+        key value pairs of the properties to be edited.
 
     Returns
     -------
-    self : iterator
-        Iterator for leaves/hash pieces.
+    dict
+        The edited and nested Meta and info dictionaries.
     """
-    return self
+    logger.debug("editing torrent file %s", metafile)
+    meta = pyben.load(metafile)
+    info = meta["info"]
+    filter_empty(args, meta, info)
+
+    if "comment" in args:
+        info["comment"] = args["comment"]
+
+    if "source" in args:
+        info["source"] = args["source"]
+
+    if "private" in args:
+        info["private"] = 1
+
+    if "announce" in args:
+        val = args.get("announce", None)
+        if isinstance(val, str):
+            vallist = val.split()
+            meta["announce"] = vallist[0]
+            meta["announce-list"] = [vallist]
+        elif isinstance(val, list):
+            meta["announce"] = val[0]
+            meta["announce-list"] = [val]
+
+    if "url-list" in args:
+        val = args.get("url-list")
+        if isinstance(val, str):
+            meta["url-list"] = val.split()
+        elif isinstance(val, list):
+            meta["url-list"] = val
+
+    if "httpseeds" in args:
+        val = args.get("httpseeds")
+        if isinstance(val, str):
+            meta["httpseeds"] = val.split()
+        elif isinstance(val, list):
+            meta["httpseeds"] = val
+
+    meta["info"] = info
+    os.remove(metafile)
+    pyben.dump(meta, metafile)
+    return meta
 
+
@@ -15512,62 +22136,118 @@

-
+
+ +

+filter_empty(args, meta, info) -

-__next__(self) - - special - +

-

-

Generate piece-length pieces of data from input file list.

+

Remove dictionary keys with empty values.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + dict +

Editable metafile properties from user.

+ required +
meta + dict +

Metafile data dictionary.

+ required +
info + dict +

Metafile info dictionary.

+ required +
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytes

SHA1 hash of the piece extracted.

- Source code in torrentfile\hasher.py -
def __next__(self) -> bytes:
+          Source code in torrentfile\edit.py
+          
46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
def filter_empty(args: dict, meta: dict, info: dict):
     """
-    Generate piece-length pieces of data from input file list.
+    Remove dictionary keys with empty values.
 
-    Returns
-    -------
-    bytes
-        SHA1 hash of the piece extracted.
+    Parameters
+    ----------
+    args : dict
+        Editable metafile properties from user.
+    meta : dict
+        Metafile data dictionary.
+    info : dict
+        Metafile info dictionary.
     """
-    while True:
-        piece = bytearray(self.piece_length)
-        size = self.current.readinto(piece)
-        if size == 0:
-            if not self.next_file():
-                raise StopIteration
-        elif size < self.piece_length:
-            self.prog_update(size)
-            return self._handle_partial(piece[:size])
-        else:
-            self.prog_update(size)
-            return sha1(piece).digest()  # nosec
+    for key, val in list(args.items()):
+        if val is None:
+            del args[key]
+            continue
+
+        if val == "":
+            if key in meta:
+                del meta[key]
+            elif key in info:
+                del info[key]
+            del args[key]
+            logger.debug("removeing empty fields %s", val)
 
+
@@ -15575,72 +22255,41 @@

-
+
-

-next_file(self) +

+
- -
-

Seemlessly transition to next file in file list.

+
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bool

True if there is a next file otherwise False.

-
- Source code in torrentfile\hasher.py -
def next_file(self) -> bool:
-    """
-    Seemlessly transition to next file in file list.
 
-    Returns
-    -------
-    bool:
-        True if there is a next file otherwise False.
-    """
-    self.index += 1
-    self.prog_close()
-    if self.index < len(self.paths):
-        path = self.paths[self.index]
-        logger.debug("Hashing %s", str(path))
-        self.current.close()
-        if self.progress:
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
-        self.current = open(path, "rb")
-        return True
-    return False
-
-
-
-
+

+ torrentfile.hasher + + + +

+ +
+ +

Piece/File Hashers for Bittorrent meta file contents.

+ + + +
-
-
-
@@ -15648,62 +22297,214 @@

-

- -HasherHybrid (CbMixin, ProgMixin) - +

+ FileHasher

+
+

+ Bases: CbMixin, ProgMixin

+

Calculate root and piece hashes for creating hybrid torrent file.

-

DEPRECATED

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
pathstr

path to target file.

required
piece_lengthint

piece length for data chunks.

required
+ - - - - + + + + - -
progressbool

default = None

TrueNameTypeDescriptionDefault
-
- Source code in torrentfile\hasher.py -
class HasherHybrid(CbMixin, ProgMixin):
+    
+    
+        
+          path
+          
+                str
+          
+          

path to target file.

+ + required + + + + piece_length + + int + +

piece length for data chunks.

+ + required + + + + progress + + int + +

default = None

+ + True + + + + + + +
+ Source code in torrentfile\hasher.py +
400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
class FileHasher(CbMixin, ProgMixin):
     """
     Calculate root and piece hashes for creating hybrid torrent file.
 
-    **DEPRECATED**
-
     Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
     With a branching factor of 2, merge layer hashes until blocks equal
     piece_length bytes for the piece layer, and then the root hash.
@@ -15718,11 +22519,15 @@ 

default = None """ - def __init__(self, path: str, piece_length: int, progress: bool = True): + def __init__( + self, + path: str, + piece_length: int, + progress: bool = True, + hybrid: bool = False, + ): """ Construct Hasher class instances for each file in torrent. - - **DEPRECATED** """ self.path = path self.piece_length = piece_length @@ -15732,18 +22537,22 @@

self.root = None self.padding_piece = None self.padding_file = None + self.amount = piece_length // BLOCK_SIZE + self.end = False + self.current = open(path, "rb") + self.hybrid = hybrid if progress: + self.progressbar = True self.prog_start(os.path.getsize(path), path, unit="bytes") - self.amount = piece_length // BLOCK_SIZE - with open(path, "rb") as data: - self.process_file(data) + + def __iter__(self): + """Return `self`: needed to implement iterator implementation.""" + return self def _pad_remaining(self, block_count: int): """ Generate Hash sized, 0 filled bytes for padding. - **DEPRECATED** - Parameters ---------- block_count : int @@ -15759,44 +22568,52 @@

if not self.layer_hashes: power2 = next_power_2(block_count) remaining = power2 - block_count - self.prog_update(HASH_SIZE * remaining) return [bytes(HASH_SIZE) for _ in range(remaining)] - def process_file(self, data: bytearray): + def __next__(self) -> bytes: """ Calculate layer hashes for contents of file. - **DEPRECATED** - Parameters ---------- data : BytesIO File opened in read mode. + + Returns + ------- + bytes + The layer merckle root hash. """ - while True: - plength = self.piece_length - blocks = [] - piece = sha1() # nosec - total = 0 - block = bytearray(BLOCK_SIZE) - for _ in range(self.amount): - size = data.readinto(block) - self.prog_update(size) - if not size: - break - total += size - plength -= size - blocks.append(sha256(block[:size]).digest()) - piece.update(block[:size]) - if not blocks: + if self.end: + self.end = False + raise StopIteration + plength = self.piece_length + blocks = [] + piece = sha1() # nosec + total = 0 + block = bytearray(BLOCK_SIZE) + for _ in range(self.amount): + size = self.current.readinto(block) + if not size: + self.end = True break - if len(blocks) != self.amount: - padding = self._pad_remaining(len(blocks)) - blocks.extend(padding) - layer_hash = merkle_root(blocks) - if self._cb: - self._cb(layer_hash) - self.layer_hashes.append(layer_hash) + total += size + plength -= size + blocks.append(sha256(block[:size]).digest()) + if self.hybrid: + piece.update(block[:size]) + if len(blocks) != self.amount: + padding = self._pad_remaining(len(blocks)) + blocks.extend(padding) + self.prog_update(total) + layer_hash = merkle_root(blocks) + self.layer_hashes.append(layer_hash) + if self._cb: + self._cb(layer_hash) + if self.end: + self._calculate_root() + self.prog_close() + if self.hybrid: if plength > 0: self.padding_file = { "attr": "p", @@ -15804,15 +22621,14 @@

"path": [".pad", str(plength)], } piece.update(bytes(plength)) - self.pieces.append(piece.digest()) # nosec - self._calculate_root() - self.prog_close() + piece = piece.digest() + self.pieces.append(piece) + return layer_hash, piece + return layer_hash def _calculate_root(self): """ Calculate the root hash for opened file. - - **DEPRECATED** """ self.piece_layer = b"".join(self.layer_hashes) @@ -15824,61 +22640,366 @@

self.layer_hashes += [pad_piece for _ in range(remainder)] self.root = merkle_root(self.layer_hashes) + self.current.close() +

+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + +

+__init__(path, piece_length, progress=True, hybrid=False) + + +

+ + +
+ +

Construct Hasher class instances for each file in torrent.

+ +
+ Source code in torrentfile\hasher.py +
418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
def __init__(
+    self,
+    path: str,
+    piece_length: int,
+    progress: bool = True,
+    hybrid: bool = False,
+):
+    """
+    Construct Hasher class instances for each file in torrent.
+    """
+    self.path = path
+    self.piece_length = piece_length
+    self.pieces = []
+    self.layer_hashes = []
+    self.piece_layer = None
+    self.root = None
+    self.padding_piece = None
+    self.padding_file = None
+    self.amount = piece_length // BLOCK_SIZE
+    self.end = False
+    self.current = open(path, "rb")
+    self.hybrid = hybrid
+    if progress:
+        self.progressbar = True
+        self.prog_start(os.path.getsize(path), path, unit="bytes")
+
+
+
+
+ +
+ + + +
+ + + +

+__iter__() + + +

+ + +
+ +

Return self: needed to implement iterator implementation.

+ +
+ Source code in torrentfile\hasher.py +
444
+445
+446
def __iter__(self):
+    """Return `self`: needed to implement iterator implementation."""
+    return self
+
+
+
+
+ +
+ + + +
+ + + +

+__next__() + + +

+ + +
+ +

Calculate layer hashes for contents of file.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
data + BytesIO +

File opened in read mode.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ bytes +

The layer merckle root hash.

+ +
+ Source code in torrentfile\hasher.py +
469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
def __next__(self) -> bytes:
+    """
+    Calculate layer hashes for contents of file.
+
+    Parameters
+    ----------
+    data : BytesIO
+        File opened in read mode.
+
+    Returns
+    -------
+    bytes
+        The layer merckle root hash.
+    """
+    if self.end:
+        self.end = False
+        raise StopIteration
+    plength = self.piece_length
+    blocks = []
+    piece = sha1()  # nosec
+    total = 0
+    block = bytearray(BLOCK_SIZE)
+    for _ in range(self.amount):
+        size = self.current.readinto(block)
+        if not size:
+            self.end = True
+            break
+        total += size
+        plength -= size
+        blocks.append(sha256(block[:size]).digest())
+        if self.hybrid:
+            piece.update(block[:size])
+    if len(blocks) != self.amount:
+        padding = self._pad_remaining(len(blocks))
+        blocks.extend(padding)
+    self.prog_update(total)
+    layer_hash = merkle_root(blocks)
+    self.layer_hashes.append(layer_hash)
+    if self._cb:
+        self._cb(layer_hash)
+    if self.end:
+        self._calculate_root()
+        self.prog_close()
+    if self.hybrid:
+        if plength > 0:
+            self.padding_file = {
+                "attr": "p",
+                "length": plength,
+                "path": [".pad", str(plength)],
+            }
+            piece.update(bytes(plength))
+        piece = piece.digest()
+        self.pieces.append(piece)
+        return layer_hash, piece
+    return layer_hash
 
+
+
- - -
- - - - - - +
-
+
-

-__init__(self, path, piece_length, progress=True) +

+_calculate_root() - - special -

+
-

Construct Hasher class instances for each file in torrent.

-

DEPRECATED

+

Calculate the root hash for opened file.

Source code in torrentfile\hasher.py -
def __init__(self, path: str, piece_length: int, progress: bool = True):
+          
525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
def _calculate_root(self):
     """
-    Construct Hasher class instances for each file in torrent.
-
-    **DEPRECATED**
+    Calculate the root hash for opened file.
     """
-    self.path = path
-    self.piece_length = piece_length
-    self.pieces = []
-    self.layer_hashes = []
-    self.piece_layer = None
-    self.root = None
-    self.padding_piece = None
-    self.padding_file = None
-    if progress:
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
-    self.amount = piece_length // BLOCK_SIZE
-    with open(path, "rb") as data:
-        self.process_file(data)
+    self.piece_layer = b"".join(self.layer_hashes)
+
+    if len(self.layer_hashes) > 1:
+        pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
+
+        pow2 = next_power_2(len(self.layer_hashes))
+        remainder = pow2 - len(self.layer_hashes)
+
+        self.layer_hashes += [pad_piece for _ in range(remainder)]
+    self.root = merkle_root(self.layer_hashes)
+    self.current.close()
 
+
@@ -15886,88 +23007,106 @@

-
+
-

-process_file(self, data) +

+_pad_remaining(block_count)

+
-

Calculate layer hashes for contents of file.

-

DEPRECATED

+

Generate Hash sized, 0 filled bytes for padding.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
databytearray

File opened in read mode.

requiredNameTypeDescriptionDefault
+ + + + block_count + + int + +

current total number of blocks collected.

+ + required + + + + + +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
padding + bytes +

Padding to fill remaining portion of tree.

+
Source code in torrentfile\hasher.py -
def process_file(self, data: bytearray):
-    """
-    Calculate layer hashes for contents of file.
-
-    **DEPRECATED**
+          
448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
def _pad_remaining(self, block_count: int):
+    """
+    Generate Hash sized, 0 filled bytes for padding.
 
     Parameters
     ----------
-    data : BytesIO
-        File opened in read mode.
+    block_count : int
+        current total number of blocks collected.
+
+    Returns
+    -------
+    padding : bytes
+        Padding to fill remaining portion of tree.
     """
-    while True:
-        plength = self.piece_length
-        blocks = []
-        piece = sha1()  # nosec
-        total = 0
-        block = bytearray(BLOCK_SIZE)
-        for _ in range(self.amount):
-            size = data.readinto(block)
-            self.prog_update(size)
-            if not size:
-                break
-            total += size
-            plength -= size
-            blocks.append(sha256(block[:size]).digest())
-            piece.update(block[:size])
-        if not blocks:
-            break
-        if len(blocks) != self.amount:
-            padding = self._pad_remaining(len(blocks))
-            blocks.extend(padding)
-        layer_hash = merkle_root(blocks)
-        if self._cb:
-            self._cb(layer_hash)
-        self.layer_hashes.append(layer_hash)
-        if plength > 0:
-            self.padding_file = {
-                "attr": "p",
-                "length": plength,
-                "path": [".pad", str(plength)],
-            }
-            piece.update(bytes(plength))
-        self.pieces.append(piece.digest())  # nosec
-    self._calculate_root()
-    self.prog_close()
+    # when the there is only one block for file
+    remaining = self.amount - block_count
+    if not self.layer_hashes:
+        power2 = next_power_2(block_count)
+        remaining = power2 - block_count
+    return [bytes(HASH_SIZE) for _ in range(remaining)]
 
+
@@ -15989,164 +23128,297 @@

-

- -HasherV2 (CbMixin, ProgMixin) - +

+ Hasher

+
+

+ Bases: CbMixin, ProgMixin

-

Calculate the root hash and piece layers for file contents.

-

DEPRECATED

-

Iterates over 16KiB blocks of data from given file, hashes the data, -then creates a hash tree from the individual block hashes until size of -hashed data equals the piece-length. Then continues the hash tree until -root hash is calculated.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - +

Piece hasher for Bittorrent V1 files.

+

Takes a sorted list of all file paths, calculates sha1 hash +for fixed size pieces of file data from each file +seemlessly until the last piece which may be smaller than others.

+ +

Parameters:

+
NameTypeDescriptionDefault
pathstr

Path to file.

required
piece_lengthint

Size of layer hashes pieces.

required
+ - - - - + + + + - -
progressbool

default = None

TrueNameTypeDescriptionDefault
-
- Source code in torrentfile\hasher.py -
class HasherV2(CbMixin, ProgMixin):
+    
+    
+        
+          paths
+          
+                list
+          
+          

List of files.

+ + required + + + + piece_length + + int + +

Size of chuncks to split the data into.

+ + required + + + + progress + + int + +

default = None

+ + True + + + + + + +
+ Source code in torrentfile\hasher.py +
 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
class Hasher(CbMixin, ProgMixin):
     """
-    Calculate the root hash and piece layers for file contents.
-
-    **DEPRECATED**
+    Piece hasher for Bittorrent V1 files.
 
-    Iterates over 16KiB blocks of data from given file, hashes the data,
-    then creates a hash tree from the individual block hashes until size of
-    hashed data equals the piece-length.  Then continues the hash tree until
-    root hash is calculated.
+    Takes a sorted list of all file paths, calculates sha1 hash
+    for fixed size pieces of file data from each file
+    seemlessly until the last piece which may be smaller than others.
 
     Parameters
     ----------
-    path : str
-        Path to file.
+    paths : list
+        List of files.
     piece_length : int
-        Size of layer hashes pieces.
+        Size of chuncks to split the data into.
     progress : int
         default = None
     """
 
-    def __init__(self, path: str, piece_length: int, progress: bool = True):
+    def __init__(self, paths: list, piece_length: int, progress: bool = True):
+        """Generate hashes of piece length data from filelist contents."""
+        self.piece_length = piece_length
+        self.paths = paths
+        self.progress = progress
+        self.total = sum([os.path.getsize(i) for i in self.paths])
+        self.index = 0
+        self.current = open(self.paths[0], "rb")
+        if self.progress:
+            total = os.path.getsize(self.paths[0])
+            self.prog_start(total, self.paths[0], unit="bytes")
+        logger.debug("Hashing %s", str(self.paths[0]))
+
+    def __iter__(self):
         """
-        Calculate and store hash information for specific file.
+        Iterate through feed pieces.
 
-        **DEPRECATED**
+        Returns
+        -------
+        self : iterator
+            Iterator for leaves/hash pieces.
         """
-        self.path = path
-        self.root = None
-        self.piece_layer = None
-        self.layer_hashes = []
-        self.piece_length = piece_length
-        self.num_blocks = piece_length // BLOCK_SIZE
-        if progress:
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
-        with open(self.path, "rb") as fd:
-            self.process_file(fd)
+        return self
 
-    def process_file(self, fd: str):
+    def _handle_partial(self, arr: bytearray) -> bytearray:
         """
-        Calculate hashes over 16KiB chuncks of file content.
-
-        **DEPRECATED**
+        Define the handling partial pieces that span 2 or more files.
 
         Parameters
         ----------
-        fd : TextIOWrapper
-            Opened file in read mode.
+        arr : bytearray
+            Incomplete piece containing partial data
+
+        Returns
+        -------
+        digest : bytearray
+            SHA1 digest of the complete piece.
         """
-        while True:
-            blocks = []
-            leaf = bytearray(BLOCK_SIZE)
-            # generate leaves of merkle tree
+        while len(arr) < self.piece_length and self.next_file():
+            target = self.piece_length - len(arr)
+            temp = bytearray(target)
+            size = self.current.readinto(temp)
+            arr.extend(temp[:size])
+            self.prog_update(size)
+            if size == target:
+                break
+        return sha1(arr).digest()  # nosec
 
-            for _ in range(self.num_blocks):
-                size = fd.readinto(leaf)
-                self.prog_update(size)
-                if not size:
-                    break
-                blocks.append(sha256(leaf[:size]).digest())
+    def next_file(self) -> bool:
+        """
+        Seemlessly transition to next file in file list.
 
-            # blocks is empty mean eof
-            if not blocks:
-                break
-            if len(blocks) != self.num_blocks:
-                # when size of file doesn't fill the last block
-                # when the file contains multiple pieces
-                remaining = self.num_blocks - len(blocks)
-                if not self.layer_hashes:
-                    # when the there is only one block for file
-                    power2 = next_power_2(len(blocks))
-                    remaining = power2 - len(blocks)
+        Returns
+        -------
+        bool:
+            True if there is a next file otherwise False.
+        """
+        self.index += 1
+        self.prog_close()
+        if self.index < len(self.paths):
+            path = self.paths[self.index]
+            logger.debug("Hashing %s", str(path))
+            self.current.close()
+            if self.progress:
+                self.prog_start(os.path.getsize(path), path, unit="bytes")
+            self.current = open(path, "rb")
+            return True
+        return False
+
+    def __next__(self) -> bytes:
+        """
+        Generate piece-length pieces of data from input file list.
+
+        Returns
+        -------
+        bytes
+            SHA1 hash of the piece extracted.
+        """
+        while True:
+            piece = bytearray(self.piece_length)
+            size = self.current.readinto(piece)
+            if size == 0:
+                if not self.next_file():
+                    raise StopIteration
+            elif size < self.piece_length:
+                self.prog_update(size)
+                return self._handle_partial(piece[:size])
+            else:
+                self.prog_update(size)
+                return sha1(piece).digest()  # nosec
+
+
+
- # pad the the rest with zeroes to fill remaining space. - padding = [bytes(32) for _ in range(remaining)] - self.prog_update(HASH_SIZE * remaining) - blocks.extend(padding) - # calculate the root hash for the merkle tree up to piece-length - layer_hash = merkle_root(blocks) - if self._cb: - self._cb(layer_hash) - self.layer_hashes.append(layer_hash) - self._calculate_root() - self.prog_close() - def _calculate_root(self): - """ - Calculate root hash for the target file. +
- **DEPRECATED** - """ - self.piece_layer = b"".join(self.layer_hashes) - hashes = len(self.layer_hashes) - if hashes > 1: - pow2 = next_power_2(hashes) - remainder = pow2 - hashes - pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)] - for _ in range(remainder): - self.layer_hashes.append(merkle_root(pad_piece)) - self.root = merkle_root(self.layer_hashes) -
-
-
@@ -16156,43 +23428,49 @@

-
+
-

-__init__(self, path, piece_length, progress=True) - - special - +

+__init__(paths, piece_length, progress=True) +

+
-

Calculate and store hash information for specific file.

-

DEPRECATED

+

Generate hashes of piece length data from filelist contents.

Source code in torrentfile\hasher.py -
def __init__(self, path: str, piece_length: int, progress: bool = True):
-    """
-    Calculate and store hash information for specific file.
-
-    **DEPRECATED**
-    """
-    self.path = path
-    self.root = None
-    self.piece_layer = None
-    self.layer_hashes = []
+          
54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
def __init__(self, paths: list, piece_length: int, progress: bool = True):
+    """Generate hashes of piece length data from filelist contents."""
     self.piece_length = piece_length
-    self.num_blocks = piece_length // BLOCK_SIZE
-    if progress:
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
-    with open(self.path, "rb") as fd:
-        self.process_file(fd)
+    self.paths = paths
+    self.progress = progress
+    self.total = sum([os.path.getsize(i) for i in self.paths])
+    self.index = 0
+    self.current = open(self.paths[0], "rb")
+    if self.progress:
+        total = os.path.getsize(self.paths[0])
+        self.prog_start(total, self.paths[0], unit="bytes")
+    logger.debug("Hashing %s", str(self.paths[0]))
 
+
@@ -16200,90 +23478,62 @@

-
+
-

-process_file(self, fd) +

+__iter__()

+
-

Calculate hashes over 16KiB chuncks of file content.

-

DEPRECATED

+

Iterate through feed pieces.

-

Parameters:

- - - - - - - - - - +

Returns:

+
NameTypeDescriptionDefault
+ - - - - + + - -
fdstr

Opened file in read mode.

requiredName TypeDescription
+ + + +self + iterator + +

Iterator for leaves/hash pieces.

+ + + +
Source code in torrentfile\hasher.py -
def process_file(self, fd: str):
+          
67
+68
+69
+70
+71
+72
+73
+74
+75
+76
def __iter__(self):
     """
-    Calculate hashes over 16KiB chuncks of file content.
-
-    **DEPRECATED**
+    Iterate through feed pieces.
 
-    Parameters
-    ----------
-    fd : TextIOWrapper
-        Opened file in read mode.
+    Returns
+    -------
+    self : iterator
+        Iterator for leaves/hash pieces.
     """
-    while True:
-        blocks = []
-        leaf = bytearray(BLOCK_SIZE)
-        # generate leaves of merkle tree
-
-        for _ in range(self.num_blocks):
-            size = fd.readinto(leaf)
-            self.prog_update(size)
-            if not size:
-                break
-            blocks.append(sha256(leaf[:size]).digest())
-
-        # blocks is empty mean eof
-        if not blocks:
-            break
-        if len(blocks) != self.num_blocks:
-            # when size of file doesn't fill the last block
-            # when the file contains multiple pieces
-            remaining = self.num_blocks - len(blocks)
-            if not self.layer_hashes:
-                # when the there is only one block for file
-                power2 = next_power_2(len(blocks))
-                remaining = power2 - len(blocks)
-
-            # pad the the rest with zeroes to fill remaining space.
-            padding = [bytes(32) for _ in range(remaining)]
-            self.prog_update(HASH_SIZE * remaining)
-            blocks.extend(padding)
-        # calculate the root hash for the merkle tree up to piece-length
-
-        layer_hash = merkle_root(blocks)
-        if self._cb:
-            self._cb(layer_hash)
-        self.layer_hashes.append(layer_hash)
-    self._calculate_root()
-    self.prog_close()
+    return self
 
+
@@ -16291,89 +23541,197 @@

+
+ -
+

+__next__() + + +

+ + +
+ +

Generate piece-length pieces of data from input file list.

+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ bytes +

SHA1 hash of the piece extracted.

+ +
+ Source code in torrentfile\hasher.py +
123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
def __next__(self) -> bytes:
+    """
+    Generate piece-length pieces of data from input file list.
 
+    Returns
+    -------
+    bytes
+        SHA1 hash of the piece extracted.
+    """
+    while True:
+        piece = bytearray(self.piece_length)
+        size = self.current.readinto(piece)
+        if size == 0:
+            if not self.next_file():
+                raise StopIteration
+        elif size < self.piece_length:
+            self.prog_update(size)
+            return self._handle_partial(piece[:size])
+        else:
+            self.prog_update(size)
+            return sha1(piece).digest()  # nosec
+
+
+
-
-

-merkle_root(blocks) +

+_handle_partial(arr) -

+

+
-

Calculate the merkle root for a seq of sha256 hash digests.

+

Define the handling partial pieces that span 2 or more files.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
arr + bytearray +

Incomplete piece containing partial data

+ required +
+ +

Returns:

+ + - - - - + + - -
blockslist

a sequence of sha256 layer hashes.

requiredName TypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytes

the sha256 root hash of the merkle tree.

+ + + +digest + bytearray + +

SHA1 digest of the complete piece.

+ + + +
Source code in torrentfile\hasher.py -
def merkle_root(blocks: list) -> bytes:
-    """
-    Calculate the merkle root for a seq of sha256 hash digests.
+          
 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
def _handle_partial(self, arr: bytearray) -> bytearray:
+    """
+    Define the handling partial pieces that span 2 or more files.
 
     Parameters
     ----------
-    blocks : list
-        a sequence of sha256 layer hashes.
+    arr : bytearray
+        Incomplete piece containing partial data
 
     Returns
     -------
-    bytes
-        the sha256 root hash of the merkle tree.
-    """
-    if blocks:
-        while len(blocks) > 1:
-            blocks = [
-                sha256(x + y).digest() for x, y in zip(*[iter(blocks)] * 2)
-            ]
-        return blocks[0]
-    return blocks
+    digest : bytearray
+        SHA1 digest of the complete piece.
+    """
+    while len(arr) < self.piece_length and self.next_file():
+        target = self.piece_length - len(arr)
+        temp = bytearray(target)
+        size = self.current.readinto(temp)
+        arr.extend(temp[:size])
+        self.prog_update(size)
+        if size == target:
+            break
+    return sha1(arr).digest()  # nosec
 
+
@@ -16381,156 +23739,435 @@

+
-
+

+next_file() + + +

+ + +
+ +

Seemlessly transition to next file in file list.

+ +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
bool + bool +

True if there is a next file otherwise False.

+ +
+ Source code in torrentfile\hasher.py +
102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
def next_file(self) -> bool:
+    """
+    Seemlessly transition to next file in file list.
 
+    Returns
+    -------
+    bool:
+        True if there is a next file otherwise False.
+    """
+    self.index += 1
+    self.prog_close()
+    if self.index < len(self.paths):
+        path = self.paths[self.index]
+        logger.debug("Hashing %s", str(path))
+        self.current.close()
+        if self.progress:
+            self.prog_start(os.path.getsize(path), path, unit="bytes")
+        self.current = open(path, "rb")
+        return True
+    return False
+
+
+
-
- -

- torrentfile.interactive +

+
+

-

-
-

Module contains the procedures used for Interactive Mode.

+
-
+

+ HasherHybrid +

+
+

+ Bases: CbMixin, ProgMixin

-
+

Calculate root and piece hashes for creating hybrid torrent file.

+

DEPRECATED

+

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. +With a branching factor of 2, merge layer hashes until blocks equal +piece_length bytes for the piece layer, and then the root hash.

+

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

path to target file.

+ required +
piece_length + int +

piece length for data chunks.

+ required +
progress + int +

default = None

+ True +
+ + +
+ Source code in torrentfile\hasher.py +
272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
class HasherHybrid(CbMixin, ProgMixin):
+    """
+    Calculate root and piece hashes for creating hybrid torrent file.
 
+    **DEPRECATED**
 
-

- -InteractiveCreator + Create merkle tree layers from sha256 hashed 16KiB blocks of contents. + With a branching factor of 2, merge layer hashes until blocks equal + piece_length bytes for the piece layer, and then the root hash. + Parameters + ---------- + path : str + path to target file. + piece_length : int + piece length for data chunks. + progress : int + default = None + """ + def __init__(self, path: str, piece_length: int, progress: bool = True): + """ + Construct Hasher class instances for each file in torrent. -

+ **DEPRECATED** + """ + self.path = path + self.piece_length = piece_length + self.pieces = [] + self.layer_hashes = [] + self.piece_layer = None + self.root = None + self.padding_piece = None + self.padding_file = None + if progress: + self.prog_start(os.path.getsize(path), path, unit="bytes") + self.amount = piece_length // BLOCK_SIZE + with open(path, "rb") as data: + self.process_file(data) -
+ def _pad_remaining(self, block_count: int): + """ + Generate Hash sized, 0 filled bytes for padding. -

Class namespace for interactive program options.

+ **DEPRECATED** -
- Source code in torrentfile\interactive.py -
class InteractiveCreator:
-    """
-    Class namespace for interactive program options.
-    """
+        Parameters
+        ----------
+        block_count : int
+            current total number of blocks collected.
 
-    def __init__(self):
-        """
-        Initialize interactive meta file creator dialog.
+        Returns
+        -------
+        padding : bytes
+            Padding to fill remaining portion of tree.
         """
-        self.kwargs = {
-            "announce": None,
-            "url_list": None,
-            "private": None,
-            "source": None,
-            "comment": None,
-            "piece_length": None,
-            "outfile": None,
-            "path": None,
-            "httpseeds": None,
-        }
-        self.outfile, self.meta = self.get_props()
+        # when the there is only one block for file
+        remaining = self.amount - block_count
+        if not self.layer_hashes:
+            power2 = next_power_2(block_count)
+            remaining = power2 - block_count
+        self.prog_update(HASH_SIZE * remaining)
+        return [bytes(HASH_SIZE) for _ in range(remaining)]
 
-    def get_props(self):
+    def process_file(self, data: bytearray):
         """
-        Gather details for torrentfile from user.
-        """
-        piece_length = get_input(
-            "Piece Length (empty=auto): ", lambda x: x.isdigit()
-        )
+        Calculate layer hashes for contents of file.
 
-        self.kwargs["piece_length"] = piece_length
-        announce = get_input(
-            "Tracker list (empty): ", lambda x: isinstance(x, str)
-        )
+        **DEPRECATED**
 
-        if announce:
-            self.kwargs["announce"] = announce.split()
+        Parameters
+        ----------
+        data : BytesIO
+            File opened in read mode.
+        """
+        while True:
+            plength = self.piece_length
+            blocks = []
+            piece = sha1()  # nosec
+            total = 0
+            block = bytearray(BLOCK_SIZE)
+            for _ in range(self.amount):
+                size = data.readinto(block)
+                self.prog_update(size)
+                if not size:
+                    break
+                total += size
+                plength -= size
+                blocks.append(sha256(block[:size]).digest())
+                piece.update(block[:size])
+            if not blocks:
+                break
+            if len(blocks) != self.amount:
+                padding = self._pad_remaining(len(blocks))
+                blocks.extend(padding)
+            layer_hash = merkle_root(blocks)
+            if self._cb:
+                self._cb(layer_hash)
+            self.layer_hashes.append(layer_hash)
+            if plength > 0:
+                self.padding_file = {
+                    "attr": "p",
+                    "length": plength,
+                    "path": [".pad", str(plength)],
+                }
+                piece.update(bytes(plength))
+            self.pieces.append(piece.digest())  # nosec
+        self._calculate_root()
+        self.prog_close()
 
-        url_list = get_input(
-            "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str)
-        )
+    def _calculate_root(self):
+        """
+        Calculate the root hash for opened file.
 
-        httpseeds = get_input(
-            "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str)
-        )
+        **DEPRECATED**
+        """
+        self.piece_layer = b"".join(self.layer_hashes)
 
-        if url_list:
-            self.kwargs["url_list"] = url_list.split()
-        if httpseeds:
-            self.kwargs["httpseeds"] = httpseeds.split()
-        comment = get_input("Comment (empty): ", None)
+        if len(self.layer_hashes) > 1:
+            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
 
-        if comment:
-            self.kwargs["comment"] = comment
-        source = get_input("Source (empty): ", None)
+            pow2 = next_power_2(len(self.layer_hashes))
+            remainder = pow2 - len(self.layer_hashes)
 
-        if source:
-            self.kwargs["source"] = source
+            self.layer_hashes += [pad_piece for _ in range(remainder)]
+        self.root = merkle_root(self.layer_hashes)
+
+
+
- private = get_input( - "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN" - ) - if private and private.lower() == "y": - self.kwargs["private"] = 1 - contents = get_input("Content Path: ", os.path.exists) - self.kwargs["path"] = contents +
- outfile = get_input( - f"Output Path ({contents}.torrent): ", - lambda x: os.path.exists(os.path.dirname(x)), - ) - if outfile: - self.kwargs["outfile"] = outfile - meta_version = get_input( - "Meta Version {1,2,3}: (1)", lambda x: x in "123" - ) - showcenter(f"creating {outfile}") - if meta_version == "3": - torrent = TorrentFileHybrid(**self.kwargs) - elif meta_version == "2": - torrent = TorrentFileV2(**self.kwargs) - else: - torrent = TorrentFile(**self.kwargs) - return torrent.write() -
-
-
@@ -16540,42 +24177,63 @@

-
+
-

-__init__(self) +

+__init__(path, piece_length, progress=True) - - special -

+
-

Initialize interactive meta file creator dialog.

+

Construct Hasher class instances for each file in torrent.

+

DEPRECATED

- Source code in torrentfile\interactive.py -
def __init__(self):
+          Source code in torrentfile\hasher.py
+          
292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
def __init__(self, path: str, piece_length: int, progress: bool = True):
     """
-    Initialize interactive meta file creator dialog.
+    Construct Hasher class instances for each file in torrent.
+
+    **DEPRECATED**
     """
-    self.kwargs = {
-        "announce": None,
-        "url_list": None,
-        "private": None,
-        "source": None,
-        "comment": None,
-        "piece_length": None,
-        "outfile": None,
-        "path": None,
-        "httpseeds": None,
-    }
-    self.outfile, self.meta = self.get_props()
+    self.path = path
+    self.piece_length = piece_length
+    self.pieces = []
+    self.layer_hashes = []
+    self.piece_layer = None
+    self.root = None
+    self.padding_piece = None
+    self.padding_file = None
+    if progress:
+        self.prog_start(os.path.getsize(path), path, unit="bytes")
+    self.amount = piece_length // BLOCK_SIZE
+    with open(path, "rb") as data:
+        self.process_file(data)
 
+
@@ -16583,91 +24241,57 @@

- - - -

-get_props(self) - - -

- -
- -

Gather details for torrentfile from user.

- -
- Source code in torrentfile\interactive.py -
def get_props(self):
-    """
-    Gather details for torrentfile from user.
-    """
-    piece_length = get_input(
-        "Piece Length (empty=auto): ", lambda x: x.isdigit()
-    )
-
-    self.kwargs["piece_length"] = piece_length
-    announce = get_input(
-        "Tracker list (empty): ", lambda x: isinstance(x, str)
-    )
-
-    if announce:
-        self.kwargs["announce"] = announce.split()
+  
- url_list = get_input( - "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str) - ) - httpseeds = get_input( - "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str) - ) - if url_list: - self.kwargs["url_list"] = url_list.split() - if httpseeds: - self.kwargs["httpseeds"] = httpseeds.split() - comment = get_input("Comment (empty): ", None) +

+_calculate_root() - if comment: - self.kwargs["comment"] = comment - source = get_input("Source (empty): ", None) - if source: - self.kwargs["source"] = source +

- private = get_input( - "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN" - ) - if private and private.lower() == "y": - self.kwargs["private"] = 1 +
- contents = get_input("Content Path: ", os.path.exists) - self.kwargs["path"] = contents +

Calculate the root hash for opened file.

+

DEPRECATED

- outfile = get_input( - f"Output Path ({contents}.torrent): ", - lambda x: os.path.exists(os.path.dirname(x)), - ) +
+ Source code in torrentfile\hasher.py +
382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
def _calculate_root(self):
+    """
+    Calculate the root hash for opened file.
 
-    if outfile:
-        self.kwargs["outfile"] = outfile
+    **DEPRECATED**
+    """
+    self.piece_layer = b"".join(self.layer_hashes)
 
-    meta_version = get_input(
-        "Meta Version {1,2,3}: (1)", lambda x: x in "123"
-    )
+    if len(self.layer_hashes) > 1:
+        pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
 
-    showcenter(f"creating {outfile}")
+        pow2 = next_power_2(len(self.layer_hashes))
+        remainder = pow2 - len(self.layer_hashes)
 
-    if meta_version == "3":
-        torrent = TorrentFileHybrid(**self.kwargs)
-    elif meta_version == "2":
-        torrent = TorrentFileV2(**self.kwargs)
-    else:
-        torrent = TorrentFile(**self.kwargs)
-    return torrent.write()
+        self.layer_hashes += [pad_piece for _ in range(remainder)]
+    self.root = merkle_root(self.layer_hashes)
 
+
@@ -16675,345 +24299,614 @@

+ -

+

+_pad_remaining(block_count) + + +

+ + +
+ +

Generate Hash sized, 0 filled bytes for padding.

+

DEPRECATED

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
block_count + int +

current total number of blocks collected.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
padding + bytes +

Padding to fill remaining portion of tree.

+ +
+ Source code in torrentfile\hasher.py +
312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
def _pad_remaining(self, block_count: int):
+    """
+    Generate Hash sized, 0 filled bytes for padding.
+
+    **DEPRECATED**
 
+    Parameters
+    ----------
+    block_count : int
+        current total number of blocks collected.
+
+    Returns
+    -------
+    padding : bytes
+        Padding to fill remaining portion of tree.
+    """
+    # when the there is only one block for file
+    remaining = self.amount - block_count
+    if not self.layer_hashes:
+        power2 = next_power_2(block_count)
+        remaining = power2 - block_count
+    self.prog_update(HASH_SIZE * remaining)
+    return [bytes(HASH_SIZE) for _ in range(remaining)]
+
+
+
-
+
-

- -InteractiveEditor +

+process_file(data) +

-

-

Interactive dialog class for torrent editing.

+

Calculate layer hashes for contents of file.

+

DEPRECATED

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
data + BytesIO +

File opened in read mode.

+ required +
- Source code in torrentfile\interactive.py -
class InteractiveEditor:
+          Source code in torrentfile\hasher.py
+          
336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
def process_file(self, data: bytearray):
     """
-    Interactive dialog class for torrent editing.
-    """
+    Calculate layer hashes for contents of file.
 
-    def __init__(self, metafile: str):
-        """
-        Initialize the Interactive torrent editor guide.
+    **DEPRECATED**
 
-        Parameters
-        ----------
-        metafile : str
-            user input string identifying the path to a torrent meta file.
-        """
-        self.metafile = metafile
-        self.meta = pyben.load(metafile)
-        self.info = self.meta["info"]
+    Parameters
+    ----------
+    data : BytesIO
+        File opened in read mode.
+    """
+    while True:
+        plength = self.piece_length
+        blocks = []
+        piece = sha1()  # nosec
+        total = 0
+        block = bytearray(BLOCK_SIZE)
+        for _ in range(self.amount):
+            size = data.readinto(block)
+            self.prog_update(size)
+            if not size:
+                break
+            total += size
+            plength -= size
+            blocks.append(sha256(block[:size]).digest())
+            piece.update(block[:size])
+        if not blocks:
+            break
+        if len(blocks) != self.amount:
+            padding = self._pad_remaining(len(blocks))
+            blocks.extend(padding)
+        layer_hash = merkle_root(blocks)
+        if self._cb:
+            self._cb(layer_hash)
+        self.layer_hashes.append(layer_hash)
+        if plength > 0:
+            self.padding_file = {
+                "attr": "p",
+                "length": plength,
+                "path": [".pad", str(plength)],
+            }
+            piece.update(bytes(plength))
+        self.pieces.append(piece.digest())  # nosec
+    self._calculate_root()
+    self.prog_close()
+
+
+
+
- self.args = { - "url-list": self.meta.get("url-list", None), - "httpseeds": self.meta.get("httpseeds", None), - "announce": self.meta.get("announce-list", None), - "source": self.info.get("source", None), - "private": self.info.get("private", None), - "comment": self.info.get("comment", None), - } +
- def show_current(self): - """ - Display the current met file information to screen. - """ - out = "Current properties and values:\n" - longest = max([len(label) for label in self.args]) + 3 - for key, val in self.args.items(): - txt = (key.title() + ":").ljust(longest) + str(val) - out += f"\t{txt}\n" - showtext(out) - def sanatize_response(self, key, response): - """ - Convert the input data into a form recognizable by the program. - Parameters - ---------- - key : str - name of the property and attribute being eddited. - response : str - User input value the property is being edited to. - """ - if key in ["announce", "url-list", "httpseeds"]: - val = response.split() - else: - val = response - self.args[key] = val - def edit_props(self): - """ - Loop continuosly for edits until user signals DONE. - """ - while True: - showcenter( - "Choose the number for a propert the needs editing." - "Enter DONE when all editing has been completed." - ) - props = { - 1: "comment", - 2: "source", - 3: "private", - 4: "tracker", - 5: "web-seed", - 6: "httpseeds", - } +
- args = { - 1: "comment", - 2: "source", - 3: "private", - 4: "announce", - 5: "url-list", - 6: "httpseeds", - } +
- txt = ", ".join((str(k) + ": " + v) for k, v in props.items()) - prop = get_input(txt) - if prop.lower() == "done": - break +
- if prop.isdigit() and 0 < int(prop) < 6: - key = props[int(prop)] - key2 = args[int(prop)] - val = self.args.get(key2) - showtext( - "Enter new property value or leave empty for no value." - ) - response = get_input(f"{key.title()} ({val}): ") - self.sanatize_response(key2, response) - else: - showtext("Invalid input: Try again.") - edit_torrent(self.metafile, self.args) - - +
-
+

+ HasherV2 +

+
+

+ Bases: CbMixin, ProgMixin

+

Calculate the root hash and piece layers for file contents.

+

DEPRECATED

+

Iterates over 16KiB blocks of data from given file, hashes the data, +then creates a hash tree from the individual block hashes until size of +hashed data equals the piece-length. Then continues the hash tree until +root hash is calculated.

-
+

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

Path to file.

+ required +
piece_length + int +

Size of layer hashes pieces.

+ required +
progress + int +

default = None

+ True +
+ + +
+ Source code in torrentfile\hasher.py +
169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
class HasherV2(CbMixin, ProgMixin):
+    """
+    Calculate the root hash and piece layers for file contents.
 
+    **DEPRECATED**
 
+    Iterates over 16KiB blocks of data from given file, hashes the data,
+    then creates a hash tree from the individual block hashes until size of
+    hashed data equals the piece-length.  Then continues the hash tree until
+    root hash is calculated.
 
-

-__init__(self, metafile) + Parameters + ---------- + path : str + Path to file. + piece_length : int + Size of layer hashes pieces. + progress : int + default = None + """ - - special - + def __init__(self, path: str, piece_length: int, progress: bool = True): + """ + Calculate and store hash information for specific file. -

+ **DEPRECATED** + """ + self.path = path + self.root = None + self.piece_layer = None + self.layer_hashes = [] + self.piece_length = piece_length + self.num_blocks = piece_length // BLOCK_SIZE + if progress: + self.prog_start(os.path.getsize(path), path, unit="bytes") + with open(self.path, "rb") as fd: + self.process_file(fd) -
+ def process_file(self, fd: str): + """ + Calculate hashes over 16KiB chuncks of file content. -

Initialize the Interactive torrent editor guide.

+ **DEPRECATED** -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
metafilestr

user input string identifying the path to a torrent meta file.

required
-
- Source code in torrentfile\interactive.py -
def __init__(self, metafile: str):
-    """
-    Initialize the Interactive torrent editor guide.
+        Parameters
+        ----------
+        fd : TextIOWrapper
+            Opened file in read mode.
+        """
+        while True:
+            blocks = []
+            leaf = bytearray(BLOCK_SIZE)
+            # generate leaves of merkle tree
 
-    Parameters
-    ----------
-    metafile : str
-        user input string identifying the path to a torrent meta file.
-    """
-    self.metafile = metafile
-    self.meta = pyben.load(metafile)
-    self.info = self.meta["info"]
+            for _ in range(self.num_blocks):
+                size = fd.readinto(leaf)
+                self.prog_update(size)
+                if not size:
+                    break
+                blocks.append(sha256(leaf[:size]).digest())
 
-    self.args = {
-        "url-list": self.meta.get("url-list", None),
-        "httpseeds": self.meta.get("httpseeds", None),
-        "announce": self.meta.get("announce-list", None),
-        "source": self.info.get("source", None),
-        "private": self.info.get("private", None),
-        "comment": self.info.get("comment", None),
-    }
-
-
-
+ # blocks is empty mean eof + if not blocks: + break + if len(blocks) != self.num_blocks: + # when size of file doesn't fill the last block + # when the file contains multiple pieces + remaining = self.num_blocks - len(blocks) + if not self.layer_hashes: + # when the there is only one block for file + power2 = next_power_2(len(blocks)) + remaining = power2 - len(blocks) -
+ # pad the the rest with zeroes to fill remaining space. + padding = [bytes(32) for _ in range(remaining)] + self.prog_update(HASH_SIZE * remaining) + blocks.extend(padding) + # calculate the root hash for the merkle tree up to piece-length + layer_hash = merkle_root(blocks) + if self._cb: + self._cb(layer_hash) + self.layer_hashes.append(layer_hash) + self._calculate_root() + self.prog_close() + def _calculate_root(self): + """ + Calculate root hash for the target file. -
+ **DEPRECATED** + """ + self.piece_layer = b"".join(self.layer_hashes) + hashes = len(self.layer_hashes) + if hashes > 1: + pow2 = next_power_2(hashes) + remainder = pow2 - hashes + pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)] + for _ in range(remainder): + self.layer_hashes.append(merkle_root(pad_piece)) + self.root = merkle_root(self.layer_hashes) +
+
+
-

-edit_props(self) +
-

-
-

Loop continuosly for edits until user signals DONE.

-
- Source code in torrentfile\interactive.py -
def edit_props(self):
-    """
-    Loop continuosly for edits until user signals DONE.
-    """
-    while True:
-        showcenter(
-            "Choose the number for a propert the needs editing."
-            "Enter DONE when all editing has been completed."
-        )
 
-        props = {
-            1: "comment",
-            2: "source",
-            3: "private",
-            4: "tracker",
-            5: "web-seed",
-            6: "httpseeds",
-        }
 
-        args = {
-            1: "comment",
-            2: "source",
-            3: "private",
-            4: "announce",
-            5: "url-list",
-            6: "httpseeds",
-        }
 
-        txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
-        prop = get_input(txt)
-        if prop.lower() == "done":
-            break
 
-        if prop.isdigit() and 0 < int(prop) < 6:
-            key = props[int(prop)]
-            key2 = args[int(prop)]
-            val = self.args.get(key2)
-            showtext(
-                "Enter new property value or leave empty for no value."
-            )
-            response = get_input(f"{key.title()} ({val}): ")
-            self.sanatize_response(key2, response)
 
-        else:
-            showtext("Invalid input: Try again.")
-    edit_torrent(self.metafile, self.args)
-
-
-
-
-
+
-

-sanatize_response(self, key, response) +

+__init__(path, piece_length, progress=True)

+
-

Convert the input data into a form recognizable by the program.

+

Calculate and store hash information for specific file.

+

DEPRECATED

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
keystr

name of the property and attribute being eddited.

required
responsestr

User input value the property is being edited to.

required
- Source code in torrentfile\interactive.py -
def sanatize_response(self, key, response):
+          Source code in torrentfile\hasher.py
+          
190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
def __init__(self, path: str, piece_length: int, progress: bool = True):
     """
-    Convert the input data into a form recognizable by the program.
+    Calculate and store hash information for specific file.
 
-    Parameters
-    ----------
-    key : str
-        name of the property and attribute being eddited.
-    response : str
-        User input value the property is being edited to.
+    **DEPRECATED**
     """
-    if key in ["announce", "url-list", "httpseeds"]:
-        val = response.split()
-    else:
-        val = response
-    self.args[key] = val
+    self.path = path
+    self.root = None
+    self.piece_layer = None
+    self.layer_hashes = []
+    self.piece_length = piece_length
+    self.num_blocks = piece_length // BLOCK_SIZE
+    if progress:
+        self.prog_start(os.path.getsize(path), path, unit="bytes")
+    with open(self.path, "rb") as fd:
+        self.process_file(fd)
 
+
@@ -17021,33 +24914,55 @@

+
-

-show_current(self) +

+_calculate_root()

+
-

Display the current met file information to screen.

+

Calculate root hash for the target file.

+

DEPRECATED

- Source code in torrentfile\interactive.py -
def show_current(self):
-    """
-    Display the current met file information to screen.
+          Source code in torrentfile\hasher.py
+          
255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
def _calculate_root(self):
+    """
+    Calculate root hash for the target file.
+
+    **DEPRECATED**
     """
-    out = "Current properties and values:\n"
-    longest = max([len(label) for label in self.args]) + 3
-    for key, val in self.args.items():
-        txt = (key.title() + ":").ljust(longest) + str(val)
-        out += f"\t{txt}\n"
-    showtext(out)
+    self.piece_layer = b"".join(self.layer_hashes)
+    hashes = len(self.layer_hashes)
+    if hashes > 1:
+        pow2 = next_power_2(hashes)
+        remainder = pow2 - hashes
+        pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)]
+        for _ in range(remainder):
+            self.layer_hashes.append(merkle_root(pad_piece))
+    self.root = merkle_root(self.layer_hashes)
 
+
@@ -17055,48 +24970,143 @@

-

- -

-
+

+process_file(fd) +

-
+
+

Calculate hashes over 16KiB chuncks of file content.

+

DEPRECATED

+

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
fd + TextIOWrapper +

Opened file in read mode.

+ required +
-

-create_torrent() +
+ Source code in torrentfile\hasher.py +
207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
def process_file(self, fd: str):
+    """
+    Calculate hashes over 16KiB chuncks of file content.
 
+    **DEPRECATED**
 
-
+    Parameters
+    ----------
+    fd : TextIOWrapper
+        Opened file in read mode.
+    """
+    while True:
+        blocks = []
+        leaf = bytearray(BLOCK_SIZE)
+        # generate leaves of merkle tree
 
-    
+ for _ in range(self.num_blocks): + size = fd.readinto(leaf) + self.prog_update(size) + if not size: + break + blocks.append(sha256(leaf[:size]).digest()) -

Create new torrent file interactively.

+ # blocks is empty mean eof + if not blocks: + break + if len(blocks) != self.num_blocks: + # when size of file doesn't fill the last block + # when the file contains multiple pieces + remaining = self.num_blocks - len(blocks) + if not self.layer_hashes: + # when the there is only one block for file + power2 = next_power_2(len(blocks)) + remaining = power2 - len(blocks) -
- Source code in torrentfile\interactive.py -
def create_torrent():
-    """
-    Create new torrent file interactively.
-    """
-    showcenter("Create Torrent")
-    showtext(
-        "\nEnter values for each of the options for the torrent creator, "
-        "or leave blank for program defaults.\nSpaces are considered item "
-        "seperators for options that accept a list of values.\nValues "
-        "enclosed in () indicate the default value, while {} holds all "
-        "valid choices available for the option.\n\n"
-    )
-    creator = InteractiveCreator()
-    return creator
+            # pad the the rest with zeroes to fill remaining space.
+            padding = [bytes(32) for _ in range(remaining)]
+            self.prog_update(HASH_SIZE * remaining)
+            blocks.extend(padding)
+        # calculate the root hash for the merkle tree up to piece-length
+
+        layer_hash = merkle_root(blocks)
+        if self._cb:
+            self._cb(layer_hash)
+        self.layer_hashes.append(layer_hash)
+    self._calculate_root()
+    self.prog_close()
 
+

@@ -17104,107 +25114,119 @@

-
- - - -

-edit_action() - - -

-
-

Edit the editable values of the torrent meta file.

+
-
- Source code in torrentfile\interactive.py -
def edit_action():
-    """
-    Edit the editable values of the torrent meta file.
-    """
-    showcenter("Edit Torrent")
-    metafile = get_input("Metafile(.torrent): ", os.path.exists)
-    dialog = InteractiveEditor(metafile)
-    dialog.show_current()
-    dialog.edit_props()
-
-

+
-

-get_input(*args) +

+merkle_root(blocks)

+
-

Determine appropriate input function to call.

+

Calculate the merkle root for a seq of sha256 hash digests.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
argstuple

Arbitrary number of args to pass to next function

()NameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

The results of the function call.

+ + + + blocks + + list + +

a sequence of sha256 layer hashes.

+ + required + + + + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ bytes +

the sha256 root hash of the merkle tree.

+
- Source code in torrentfile\interactive.py -
def get_input(*args: tuple):  # pragma: no cover
+          Source code in torrentfile\hasher.py
+          
146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
def merkle_root(blocks: list) -> bytes:
     """
-    Determine appropriate input function to call.
+    Calculate the merkle root for a seq of sha256 hash digests.
 
     Parameters
     ----------
-    args : tuple
-        Arbitrary number of args to pass to next function
+    blocks : list
+        a sequence of sha256 layer hashes.
 
     Returns
     -------
-    str
-        The results of the function call.
+    bytes
+        the sha256 root hash of the merkle tree.
     """
-    if len(args) == 2:
-        return _get_input_loop(*args)
-    return _get_input(*args)
+    if blocks:
+        while len(blocks) > 1:
+            blocks = [
+                sha256(x + y).digest() for x, y in zip(*[iter(blocks)] * 2)
+            ]
+        return blocks[0]
+    return blocks
 
+
@@ -17212,143 +25234,253 @@

-
+
-

-recheck_torrent() +

+
- -
-

Check torrent download completed percentage.

+
-
- Source code in torrentfile\interactive.py -
def recheck_torrent():
-    """
-    Check torrent download completed percentage.
-    """
-    showcenter("Check Torrent")
-    msg = "Enter path to torrent contents, and corresponding torrent metafile."
-    showtext(msg)
-    metafile = get_input(
-        "Conent Path (downloads/complete/torrentname):", os.path.exists
-    )
-    contents = get_input("Metafile (*.torrent): ", os.path.exists)
-    checker = Checker(metafile, contents)
-    results = checker.results()
-    showtext(f"Completion for {metafile} is {results}%")
-    return results
-
-
-
-
+ +

+ torrentfile.interactive -
+

+
+

Module contains the procedures used for Interactive Mode.

+ + + +
+ + + + + + + +
+ + + +

+ InteractiveCreator -

-select_action()

+
-

Operate TorrentFile program interactively through terminal.

-
- Source code in torrentfile\interactive.py -
def select_action():
+      

Class namespace for interactive program options.

+ + +
+ Source code in torrentfile\interactive.py +
293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
class InteractiveCreator:
     """
-    Operate TorrentFile program interactively through terminal.
+    Class namespace for interactive program options.
     """
-    showcenter("TorrentFile: Starting Interactive Mode")
-    action = get_input(
-        "Enter the action you wish to perform.\n"
-        "Action ( Create (c) | Edit (e) | Recheck (r) ): "
-    )
-    action = action.lower()
 
-    if "create" in action or action == "c":
-        return create_torrent()
+    def __init__(self):
+        """
+        Initialize interactive meta file creator dialog.
+        """
+        self.kwargs = {
+            "announce": None,
+            "url_list": None,
+            "private": None,
+            "source": None,
+            "comment": None,
+            "piece_length": None,
+            "outfile": None,
+            "path": None,
+            "httpseeds": None,
+        }
+        self.outfile, self.meta = self.get_props()
 
-    if "check" in action or action == "r":
-        return recheck_torrent()
+    def get_props(self):
+        """
+        Gather details for torrentfile from user.
+        """
+        piece_length = get_input(
+            "Piece Length (empty=auto): ", lambda x: x.isdigit()
+        )
 
-    if "edit" in action or action == "e":
-        return edit_action()
-    print("Unable to recognize input.  Please try again.")  # pragma: nocover
-    return select_action()  # pragma: nocover
-
- - + self.kwargs["piece_length"] = piece_length + announce = get_input( + "Tracker list (empty): ", lambda x: isinstance(x, str) + ) - + if announce: + self.kwargs["announce"] = announce.split() + url_list = get_input( + "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str) + ) + httpseeds = get_input( + "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str) + ) -
+ if url_list: + self.kwargs["url_list"] = url_list.split() + if httpseeds: + self.kwargs["httpseeds"] = httpseeds.split() + comment = get_input("Comment (empty): ", None) + if comment: + self.kwargs["comment"] = comment + source = get_input("Source (empty): ", None) + if source: + self.kwargs["source"] = source -

-showcenter(txt) + private = get_input( + "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN" + ) + if private and private.lower() == "y": + self.kwargs["private"] = 1 -

+ contents = get_input("Content Path: ", os.path.exists) + self.kwargs["path"] = contents -
+ outfile = get_input( + f"Output Path ({contents}.torrent): ", + lambda x: os.path.exists(os.path.dirname(x)), + ) -

Print text to screen in the center position of the terminal.

+ if outfile: + self.kwargs["outfile"] = outfile -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
txtstr

the preformated message to send to stdout.

required
-
- Source code in torrentfile\interactive.py -
def showcenter(txt: str):
-    """
-    Print text to screen in the center position of the terminal.
+        meta_version = get_input(
+            "Meta Version {1,2,3}: (1)", lambda x: x in "123"
+        )
 
-    Parameters
-    ----------
-    txt : str
-        the preformated message to send to stdout.
-    """
-    termlen = shutil.get_terminal_size().columns
-    padding = " " * int(((termlen - len(txt)) / 2))
-    string = "".join(["\n", padding, txt, "\n"])
-    showtext(string)
+        showcenter(f"creating {outfile}")
+
+        if meta_version == "3":
+            torrent = TorrentFileHybrid(**self.kwargs)
+        elif meta_version == "2":
+            torrent = TorrentFileV2(**self.kwargs)
+        else:
+            torrent = TorrentFile(**self.kwargs)
+        return torrent.write()
 
-
-
+
+
+ + + +
+ + + + + -
@@ -17356,48 +25488,52 @@

-

-showtext(txt) +

+__init__() + +

-
-

Print contents of txt to screen.

+

Initialize interactive meta file creator dialog.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
txtstr

text to print to terminal.

required
Source code in torrentfile\interactive.py -
def showtext(txt):
+          
298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
def __init__(self):
     """
-    Print contents of txt to screen.
-
-    Parameters
-    ----------
-    txt : str
-        text to print to terminal.
+    Initialize interactive meta file creator dialog.
     """
-    sys.stdout.write(txt)
+    self.kwargs = {
+        "announce": None,
+        "url_list": None,
+        "private": None,
+        "source": None,
+        "comment": None,
+        "piece_length": None,
+        "outfile": None,
+        "path": None,
+        "httpseeds": None,
+    }
+    self.outfile, self.meta = self.get_props()
 
+
@@ -17405,48 +25541,174 @@

+
-
+

+get_props() -

-
+ +
-
+

Gather details for torrentfile from user.

+
+ Source code in torrentfile\interactive.py +
315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
def get_props(self):
+    """
+    Gather details for torrentfile from user.
+    """
+    piece_length = get_input(
+        "Piece Length (empty=auto): ", lambda x: x.isdigit()
+    )
 
+    self.kwargs["piece_length"] = piece_length
+    announce = get_input(
+        "Tracker list (empty): ", lambda x: isinstance(x, str)
+    )
 
-

- torrentfile.recheck + if announce: + self.kwargs["announce"] = announce.split() + url_list = get_input( + "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str) + ) + httpseeds = get_input( + "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str) + ) -

+ if url_list: + self.kwargs["url_list"] = url_list.split() + if httpseeds: + self.kwargs["httpseeds"] = httpseeds.split() + comment = get_input("Comment (empty): ", None) -
+ if comment: + self.kwargs["comment"] = comment + source = get_input("Source (empty): ", None) -

Module container Checker Class.

-

The CheckerClass takes a torrentfile and tha path to it's contents. -It will then iterate through every file and directory contained -and compare their data to values contained within the torrent file. -Completion percentages will be printed to screen for each file and -at the end for the torrentfile as a whole.

+ if source: + self.kwargs["source"] = source + + private = get_input( + "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN" + ) + if private and private.lower() == "y": + self.kwargs["private"] = 1 + contents = get_input("Content Path: ", os.path.exists) + self.kwargs["path"] = contents -
+ outfile = get_input( + f"Output Path ({contents}.torrent): ", + lambda x: os.path.exists(os.path.dirname(x)), + ) + + if outfile: + self.kwargs["outfile"] = outfile + + meta_version = get_input( + "Meta Version {1,2,3}: (1)", lambda x: x in "123" + ) + + showcenter(f"creating {outfile}") + + if meta_version == "3": + torrent = TorrentFileHybrid(**self.kwargs) + elif meta_version == "2": + torrent = TorrentFileV2(**self.kwargs) + else: + torrent = TorrentFile(**self.kwargs) + return torrent.write() +
+
+
+
+
+
+
+
@@ -17454,431 +25716,534 @@

-

- -Checker (ProgMixin) - +

+ InteractiveEditor

-
- -

Check a given file or directory to see if it matches a torrentfile.

-

Public constructor for Checker class instance.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
metafilestr

Path to ".torrent" file.

required
pathstr

Path where the content is located in filesystem.

required
-

Examples:

-
-
-

metafile = "/path/to/torrentfile/content_file_or_dir.torrent" - >> location = "/path/to/location" - >> os.path.exists("/path/to/location/content_file_or_dir") - Out: True - >> checker = Checker(metafile, location)

-
-
+
-
- Source code in torrentfile\recheck.py -
class Checker(ProgMixin):
-    """
-    Check a given file or directory to see if it matches a torrentfile.
 
-    Public constructor for Checker class instance.
+      

Interactive dialog class for torrent editing.

- Parameters - ---------- - metafile : str - Path to ".torrent" file. - path : str - Path where the content is located in filesystem. - Example - ------- - >> metafile = "/path/to/torrentfile/content_file_or_dir.torrent" - >> location = "/path/to/location" - >> os.path.exists("/path/to/location/content_file_or_dir") - Out: True - >> checker = Checker(metafile, location) +
+ Source code in torrentfile\interactive.py +
190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
class InteractiveEditor:
+    """
+    Interactive dialog class for torrent editing.
     """
 
-    _hook = None
-
-    def __init__(self, metafile: str, path: str):
+    def __init__(self, metafile: str):
         """
-        Validate data against hashes contained in .torrent file.
+        Initialize the Interactive torrent editor guide.
 
         Parameters
         ----------
         metafile : str
-            path to .torrent file
-        path : str
-            path to content or contents parent directory.
+            user input string identifying the path to a torrent meta file.
         """
-        if not os.path.exists(metafile):
-            raise FileNotFoundError
-        meta = []
-        thread = Thread(target=pyben.loadinto, args=(metafile, meta))
-        thread.start()
-        self.last_log = None
-        self.log_msg("Checking: %s, %s", metafile, path)
         self.metafile = metafile
-        self.total = 0
-        self.paths = []
-        self.fileinfo = {}
-        thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
-        if not meta:  # pragma: nocover
-            thread2.start()
-            thread2.join()
-        self.meta = meta[0]
+        self.meta = pyben.load(metafile)
         self.info = self.meta["info"]
-        self.name = self.info["name"]
-        self.piece_length = self.info["piece length"]
 
-        if "meta version" in self.info:
-            if "pieces" in self.info:
-                self.meta_version = 3
+        self.args = {
+            "url-list": self.meta.get("url-list", None),
+            "httpseeds": self.meta.get("httpseeds", None),
+            "announce": self.meta.get("announce-list", None),
+            "source": self.info.get("source", None),
+            "private": self.info.get("private", None),
+            "comment": self.info.get("comment", None),
+        }
+
+    def show_current(self):
+        """
+        Display the current met file information to screen.
+        """
+        out = "Current properties and values:\n"
+        longest = max([len(label) for label in self.args]) + 3
+        for key, val in self.args.items():
+            txt = (key.title() + ":").ljust(longest) + str(val)
+            out += f"\t{txt}\n"
+        showtext(out)
+
+    def sanatize_response(self, key, response):
+        """
+        Convert the input data into a form recognizable by the program.
+
+        Parameters
+        ----------
+        key : str
+            name of the property and attribute being eddited.
+        response : str
+            User input value the property is being edited to.
+        """
+        if key in ["announce", "url-list", "httpseeds"]:
+            val = response.split()
+        else:
+            val = response
+        self.args[key] = val
+
+    def edit_props(self):
+        """
+        Loop continuosly for edits until user signals DONE.
+        """
+        while True:
+            showcenter(
+                "Choose the number for a propert the needs editing."
+                "Enter DONE when all editing has been completed."
+            )
+
+            props = {
+                1: "comment",
+                2: "source",
+                3: "private",
+                4: "tracker",
+                5: "web-seed",
+                6: "httpseeds",
+            }
+
+            args = {
+                1: "comment",
+                2: "source",
+                3: "private",
+                4: "announce",
+                5: "url-list",
+                6: "httpseeds",
+            }
+
+            txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
+            prop = get_input(txt)
+            if prop.lower() == "done":
+                break
+
+            if prop.isdigit() and 0 < int(prop) < 6:
+                key = props[int(prop)]
+                key2 = args[int(prop)]
+                val = self.args.get(key2)
+                showtext(
+                    "Enter new property value or leave empty for no value."
+                )
+                response = get_input(f"{key.title()} ({val}): ")
+                self.sanatize_response(key2, response)
+
             else:
-                self.meta_version = 2
-        else:
-            self.meta_version = 1
+                showtext("Invalid input: Try again.")
+        edit_torrent(self.metafile, self.args)
+
+
+
- self.root = self.find_root(path) - self.check_paths() - @classmethod - def register_callback(cls, hook): - """ - Register hooks from 3rd party programs to access generated info. - Parameters - ---------- - hook : function - callback function for the logging feature. - """ - cls._hook = hook +
- def piece_checker(self): - """ - Check individual pieces of the torrent. - Returns - ------- - HashChecker | FeedChecker - Individual piece hasher. - """ - if self.meta_version == 1: - return FeedChecker - return HashChecker - def results(self): - """ - Generate result percentage and store for future calls. - """ - responses = [] - for response in self.iter_hashes(): - responses.append(response) - self.log_msg( - "Final result for %s recheck: %s", self.metafile, self._result - ) - return self._result - def log_msg(self, *args, level=logging.INFO): - """ - Log message `msg` to logger and send `msg` to callback hook. - Parameters - ---------- - args : dict - formatting args for log message - level : int - Log level for this message; default=`logging.INFO` - """ - message = args[0] - if len(args) >= 3: - message = message % tuple(args[1:]) - elif len(args) == 2: - message = message % args[1] - # Repeat log messages should be ignored. - if message != self.last_log: - self.last_log = message - logger.log(level, message) - if self._hook and level == logging.INFO: - self._hook(message) - def find_root(self, path: str) -> str: - """ - Check path for torrent content. - The path can be a relative or absolute filesystem path. In the case - where the content is a single file, the path may point directly to the - the file, or it may point to the parent directory. If content points - to a directory. The directory will be checked to see if it matches - the torrent's name, if not the directories contents will be searched. - The returned value will be the absolute path that matches the torrent's - name. - Parameters - ---------- - path : str - root path to torrent content - Returns - ------- - str - root path to content - """ - if not os.path.exists(path): - self.log_msg("Could not locate torrent content %s.", path) - raise FileNotFoundError(path) - root = Path(path) - if root.name == self.name: - self.log_msg("Content found: %s.", str(root)) - return root +
- if self.name in os.listdir(root): - return root / self.name - self.log_msg("Could not locate torrent content in: %s", str(root)) - raise FileNotFoundError(root) - def check_paths(self): - """ - Gather all file paths described in the torrent file. - """ - finfo = self.fileinfo +

+__init__(metafile) - if "length" in self.info: - self.log_msg("%s points to a single file", self.root) - self.total = self.info["length"] - self.paths.append(str(self.root)) - finfo[0] = { - "path": self.root, - "length": self.info["length"], - } +

- if self.meta_version > 1: - root = self.info["file tree"][self.name][""]["pieces root"] - finfo[0]["pieces root"] = root - return +
- # Otherwise Content is more than 1 file. - self.log_msg("%s points to a directory", self.root) - if self.meta_version == 1: +

Initialize the Interactive torrent editor guide.

- for i, item in enumerate(self.info["files"]): - self.total += item["length"] - base = os.path.join(*item["path"]) +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
metafile + str +

user input string identifying the path to a torrent meta file.

+ required +
- self.fileinfo[i] = { - "path": str(self.root / base), - "length": item["length"], - } +
+ Source code in torrentfile\interactive.py +
195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
def __init__(self, metafile: str):
+    """
+    Initialize the Interactive torrent editor guide.
 
-                self.paths.append(str(self.root / base))
-            return
+    Parameters
+    ----------
+    metafile : str
+        user input string identifying the path to a torrent meta file.
+    """
+    self.metafile = metafile
+    self.meta = pyben.load(metafile)
+    self.info = self.meta["info"]
 
-        self.walk_file_tree(self.info["file tree"], [])
+    self.args = {
+        "url-list": self.meta.get("url-list", None),
+        "httpseeds": self.meta.get("httpseeds", None),
+        "announce": self.meta.get("announce-list", None),
+        "source": self.info.get("source", None),
+        "private": self.info.get("private", None),
+        "comment": self.info.get("comment", None),
+    }
+
+
+
+
- def walk_file_tree(self, tree: dict, partials: list): - """ - Traverse File Tree dictionary to get file details. +
- Extract full pathnames, length, root hash, and layer hashes - for each file included in the .torrent's file tree. - Parameters - ---------- - tree : dict - File Tree dict extracted from torrent file. - partials : list - list of intermediate pathnames. - """ - for key, val in tree.items(): - # Empty string means the tree's leaf is value - if "" in val: +
- base = os.path.join(*partials, key) - roothash = None - length = val[""]["length"] - roothash = None if not length else val[""]["pieces root"] - full = str(self.root / base) - self.fileinfo[len(self.paths)] = { - "path": full, - "length": length, - "pieces root": roothash, - } - self.paths.append(full) - self.total += length - else: - self.walk_file_tree(val, partials + [key]) - def iter_hashes(self) -> tuple: - """ - Produce results of comparing torrent contents piece by piece. - Yields - ------ - chunck : bytes - hash of data found on disk - piece : bytes - hash of data when complete and correct - path : str - path to file being hashed - size : int - length of bytes hashed for piece - """ - matched = consumed = 0 - checker = self.piece_checker() - for chunk, piece, path, size in checker(self): - consumed += size - matching = 0 - if chunk == piece: - matching += size - matched += size - yield chunk, piece, path, size - total_consumed = str(int(consumed / self.total * 100)) - percent_matched = str(int(matched / consumed * 100)) - self.log_msg( - "Processed: %s%%, Matched: %s%%", - total_consumed, - percent_matched, - level=logging.DEBUG, - ) - self._result = (matched / consumed) * 100 if consumed > 0 else 0 -
-
+

+edit_props() + +

-
+
+ +

Loop continuosly for edits until user signals DONE.

+ +
+ Source code in torrentfile\interactive.py +
245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
def edit_props(self):
+    """
+    Loop continuosly for edits until user signals DONE.
+    """
+    while True:
+        showcenter(
+            "Choose the number for a propert the needs editing."
+            "Enter DONE when all editing has been completed."
+        )
 
+        props = {
+            1: "comment",
+            2: "source",
+            3: "private",
+            4: "tracker",
+            5: "web-seed",
+            6: "httpseeds",
+        }
 
+        args = {
+            1: "comment",
+            2: "source",
+            3: "private",
+            4: "announce",
+            5: "url-list",
+            6: "httpseeds",
+        }
 
+        txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
+        prop = get_input(txt)
+        if prop.lower() == "done":
+            break
 
+        if prop.isdigit() and 0 < int(prop) < 6:
+            key = props[int(prop)]
+            key2 = args[int(prop)]
+            val = self.args.get(key2)
+            showtext(
+                "Enter new property value or leave empty for no value."
+            )
+            response = get_input(f"{key.title()} ({val}): ")
+            self.sanatize_response(key2, response)
 
+        else:
+            showtext("Invalid input: Try again.")
+    edit_torrent(self.metafile, self.args)
+
+
+
+
+
-
+
-

-__init__(self, metafile, path) +

+sanatize_response(key, response) - - special -

+
-

Validate data against hashes contained in .torrent file.

+

Convert the input data into a form recognizable by the program.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - - - - - - - -
metafilestr

path to .torrent file

requiredNameTypeDescriptionDefault
pathstr

path to content or contents parent directory.

required
+ + + + key + + str + +

name of the property and attribute being eddited.

+ + required + + + + response + + str + +

User input value the property is being edited to.

+ + required + + + + +
- Source code in torrentfile\recheck.py -
def __init__(self, metafile: str, path: str):
+          Source code in torrentfile\interactive.py
+          
228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
def sanatize_response(self, key, response):
     """
-    Validate data against hashes contained in .torrent file.
+    Convert the input data into a form recognizable by the program.
 
     Parameters
     ----------
-    metafile : str
-        path to .torrent file
-    path : str
-        path to content or contents parent directory.
+    key : str
+        name of the property and attribute being eddited.
+    response : str
+        User input value the property is being edited to.
     """
-    if not os.path.exists(metafile):
-        raise FileNotFoundError
-    meta = []
-    thread = Thread(target=pyben.loadinto, args=(metafile, meta))
-    thread.start()
-    self.last_log = None
-    self.log_msg("Checking: %s, %s", metafile, path)
-    self.metafile = metafile
-    self.total = 0
-    self.paths = []
-    self.fileinfo = {}
-    thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
-    if not meta:  # pragma: nocover
-        thread2.start()
-        thread2.join()
-    self.meta = meta[0]
-    self.info = self.meta["info"]
-    self.name = self.info["name"]
-    self.piece_length = self.info["piece length"]
-
-    if "meta version" in self.info:
-        if "pieces" in self.info:
-            self.meta_version = 3
-        else:
-            self.meta_version = 2
+    if key in ["announce", "url-list", "httpseeds"]:
+        val = response.split()
     else:
-        self.meta_version = 1
-
-    self.root = self.find_root(path)
-    self.check_paths()
+        val = response
+    self.args[key] = val
 
+
@@ -17886,62 +26251,154 @@

-
+
-

-check_paths(self) +

+show_current()

+
-

Gather all file paths described in the torrent file.

+

Display the current met file information to screen.

- Source code in torrentfile\recheck.py -
def check_paths(self):
+          Source code in torrentfile\interactive.py
+          
217
+218
+219
+220
+221
+222
+223
+224
+225
+226
def show_current(self):
     """
-    Gather all file paths described in the torrent file.
+    Display the current met file information to screen.
     """
-    finfo = self.fileinfo
+    out = "Current properties and values:\n"
+    longest = max([len(label) for label in self.args]) + 3
+    for key, val in self.args.items():
+        txt = (key.title() + ":").ljust(longest) + str(val)
+        out += f"\t{txt}\n"
+    showtext(out)
+
+
+
+
- if "length" in self.info: - self.log_msg("%s points to a single file", self.root) - self.total = self.info["length"] - self.paths.append(str(self.root)) +
- finfo[0] = { - "path": self.root, - "length": self.info["length"], - } - if self.meta_version > 1: - root = self.info["file tree"][self.name][""]["pieces root"] - finfo[0]["pieces root"] = root - return - # Otherwise Content is more than 1 file. - self.log_msg("%s points to a directory", self.root) - if self.meta_version == 1: - for i, item in enumerate(self.info["files"]): - self.total += item["length"] - base = os.path.join(*item["path"]) +
- self.fileinfo[i] = { - "path": str(self.root / base), - "length": item["length"], - } +

- self.paths.append(str(self.root / base)) - return +
- self.walk_file_tree(self.info["file tree"], []) + + + +
+ + + +

+_get_input(txt) + + +

+ + +
+ +

Gather information needed from user.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
txt + str +

The message usually containing instructions for the user.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

The text input received from the user.

+ +
+ Source code in torrentfile\interactive.py +
53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
def _get_input(txt: str):  # pragma: no cover
+    """
+    Gather information needed from user.
+
+    Parameters
+    ----------
+    txt : str
+        The message usually containing instructions for the user.
+
+    Returns
+    -------
+    str
+        The text input received from the user.
+    """
+    value = input(txt)
+    return value
 
+
@@ -17949,100 +26406,122 @@

-
+
-

-find_root(self, path) +

+_get_input_loop(txt, func) -

+

+
-

Check path for torrent content.

-

The path can be a relative or absolute filesystem path. In the case -where the content is a single file, the path may point directly to the -the file, or it may point to the parent directory. If content points -to a directory. The directory will be checked to see if it matches -the torrent's name, if not the directories contents will be searched. -The returned value will be the absolute path that matches the torrent's -name.

+

Gather information needed from user.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
pathstr

root path to torrent content

requiredNameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

root path to content

-
- Source code in torrentfile\recheck.py -
def find_root(self, path: str) -> str:
-    """
-    Check path for torrent content.
+    
+    
+        
+          txt
+          
+                str
+          
+          

The message usually containing instructions for the user.

+ + required + + + + func + + function + +

Validate/Check user input data, failure = retry, success = continue.

+ + required + + + + + +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

The text input received from the user.

- The path can be a relative or absolute filesystem path. In the case - where the content is a single file, the path may point directly to the - the file, or it may point to the parent directory. If content points - to a directory. The directory will be checked to see if it matches - the torrent's name, if not the directories contents will be searched. - The returned value will be the absolute path that matches the torrent's - name. +
+ Source code in torrentfile\interactive.py +
71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
def _get_input_loop(txt, func):  # pragma: no cover
+    """
+    Gather information needed from user.
 
     Parameters
     ----------
-    path : str
-        root path to torrent content
+    txt : str
+        The message usually containing instructions for the user.
+    func : function
+        Validate/Check user input data, failure = retry, success = continue.
 
     Returns
     -------
     str
-        root path to content
+        The text input received from the user.
     """
-    if not os.path.exists(path):
-        self.log_msg("Could not locate torrent content %s.", path)
-        raise FileNotFoundError(path)
-
-    root = Path(path)
-    if root.name == self.name:
-        self.log_msg("Content found: %s.", str(root))
-        return root
-
-    if self.name in os.listdir(root):
-        return root / self.name
-
-    self.log_msg("Could not locate torrent content in: %s", str(root))
-    raise FileNotFoundError(root)
+    while True:
+        value = input(txt)
+        if func and func(value):
+            return value
+        if not func or value == "":
+            return value
+        showtext(f"Invalid input {value}: try again")
 
+
@@ -18050,71 +26529,95 @@

-
+
-

-iter_hashes(self) +

+create_torrent() -

+

+
-

Produce results of comparing torrent contents piece by piece.

+

Create new torrent file interactively.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
tuple

hash of data found on disk

- Source code in torrentfile\recheck.py -
def iter_hashes(self) -> tuple:
+          Source code in torrentfile\interactive.py
+          
163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
def create_torrent():
     """
-    Produce results of comparing torrent contents piece by piece.
+    Create new torrent file interactively.
+    """
+    showcenter("Create Torrent")
+    showtext(
+        "\nEnter values for each of the options for the torrent creator, "
+        "or leave blank for program defaults.\nSpaces are considered item "
+        "seperators for options that accept a list of values.\nValues "
+        "enclosed in () indicate the default value, while {} holds all "
+        "valid choices available for the option.\n\n"
+    )
+    creator = InteractiveCreator()
+    return creator
+
+
+
+
- Yields - ------ - chunck : bytes - hash of data found on disk - piece : bytes - hash of data when complete and correct - path : str - path to file being hashed - size : int - length of bytes hashed for piece +
+ + + +
+ + + +

+edit_action() + + +

+ + +
+ +

Edit the editable values of the torrent meta file.

+ +
+ Source code in torrentfile\interactive.py +
179
+180
+181
+182
+183
+184
+185
+186
+187
def edit_action():
+    """
+    Edit the editable values of the torrent meta file.
     """
-    matched = consumed = 0
-    checker = self.piece_checker()
-    for chunk, piece, path, size in checker(self):
-        consumed += size
-        matching = 0
-        if chunk == piece:
-            matching += size
-            matched += size
-        yield chunk, piece, path, size
-        total_consumed = str(int(consumed / self.total * 100))
-        percent_matched = str(int(matched / consumed * 100))
-        self.log_msg(
-            "Processed: %s%%, Matched: %s%%",
-            total_consumed,
-            percent_matched,
-            level=logging.DEBUG,
-        )
-    self._result = (matched / consumed) * 100 if consumed > 0 else 0
+    showcenter("Edit Torrent")
+    metafile = get_input("Metafile(.torrent): ", os.path.exists)
+    dialog = InteractiveEditor(metafile)
+    dialog.show_current()
+    dialog.edit_props()
 
+
@@ -18122,71 +26625,100 @@

-
+
-

-log_msg(self, *args, *, level=20) +

+get_input(*args) -

+

+
-

Log message msg to logger and send msg to callback hook.

+

Determine appropriate input function to call.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + + + + + + + + + + +
argsdict

formatting args for log message

()NameTypeDescriptionDefault
args + tuple +

Arbitrary number of args to pass to next function

+ required +
+ +

Returns:

+ + - - - - + + - -
levelint

Log level for this message; default=logging.INFO

20TypeDescription
+ + + + + str + +

The results of the function call.

+ + + +
- Source code in torrentfile\recheck.py -
def log_msg(self, *args, level=logging.INFO):
+          Source code in torrentfile\interactive.py
+          
34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
def get_input(*args: tuple):  # pragma: no cover
     """
-    Log message `msg` to logger and send `msg` to callback hook.
+    Determine appropriate input function to call.
 
     Parameters
     ----------
-    args : dict
-        formatting args for log message
-    level : int
-        Log level for this message; default=`logging.INFO`
-    """
-    message = args[0]
-    if len(args) >= 3:
-        message = message % tuple(args[1:])
-    elif len(args) == 2:
-        message = message % args[1]
+    args : tuple
+        Arbitrary number of args to pass to next function
 
-    # Repeat log messages should be ignored.
-    if message != self.last_log:
-        self.last_log = message
-        logger.log(level, message)
-        if self._hook and level == logging.INFO:
-            self._hook(message)
+    Returns
+    -------
+    str
+        The results of the function call.
+    """
+    if len(args) == 2:
+        return _get_input_loop(*args)
+    return _get_input(*args)
 
+
@@ -18194,50 +26726,54 @@

-
+
-

-piece_checker(self) +

+recheck_torrent() -

+

+
-

Check individual pieces of the torrent.

+

Check torrent download completed percentage.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
HashChecker | FeedChecker

Individual piece hasher.

- Source code in torrentfile\recheck.py -
def piece_checker(self):
+          Source code in torrentfile\interactive.py
+          
146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
def recheck_torrent():
     """
-    Check individual pieces of the torrent.
-
-    Returns
-    -------
-    HashChecker | FeedChecker
-        Individual piece hasher.
+    Check torrent download completed percentage.
     """
-    if self.meta_version == 1:
-        return FeedChecker
-    return HashChecker
+    showcenter("Check Torrent")
+    msg = "Enter path to torrent contents, and corresponding torrent metafile."
+    showtext(msg)
+    metafile = get_input(
+        "Conent Path (downloads/complete/torrentname):", os.path.exists
+    )
+    contents = get_input("Metafile (*.torrent): ", os.path.exists)
+    checker = Checker(metafile, contents)
+    results = checker.results()
+    showtext(f"Completion for {metafile} is {results}%")
+    return results
 
+
@@ -18245,56 +26781,66 @@

-
+
-

-register_callback(hook) +

+select_action() - - classmethod - -

+

+
-

Register hooks from 3rd party programs to access generated info.

+

Operate TorrentFile program interactively through terminal.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
hookfunction

callback function for the logging feature.

required
- Source code in torrentfile\recheck.py -
@classmethod
-def register_callback(cls, hook):
+          Source code in torrentfile\interactive.py
+          
123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
def select_action():
     """
-    Register hooks from 3rd party programs to access generated info.
-
-    Parameters
-    ----------
-    hook : function
-        callback function for the logging feature.
+    Operate TorrentFile program interactively through terminal.
     """
-    cls._hook = hook
+    showcenter("TorrentFile: Starting Interactive Mode")
+    action = get_input(
+        "Enter the action you wish to perform.\n"
+        "Action ( Create (c) | Edit (e) | Recheck (r) ): "
+    )
+    action = action.lower()
+
+    if "create" in action or action == "c":
+        return create_torrent()
+
+    if "check" in action or action == "r":
+        return recheck_torrent()
+
+    if "edit" in action or action == "e":
+        return edit_action()
+    print("Unable to recognize input.  Please try again.")  # pragma: nocover
+    return select_action()  # pragma: nocover
 
+
@@ -18302,35 +26848,74 @@

-
+
-

-results(self) +

+showcenter(txt) -

+

+
-

Generate result percentage and store for future calls.

+

Print text to screen in the center position of the terminal.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
txt + str +

the preformated message to send to stdout.

+ required +
- Source code in torrentfile\recheck.py -
def results(self):
+          Source code in torrentfile\interactive.py
+          
108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
def showcenter(txt: str):
     """
-    Generate result percentage and store for future calls.
-    """
-    responses = []
-    for response in self.iter_hashes():
-        responses.append(response)
+    Print text to screen in the center position of the terminal.
 
-    self.log_msg(
-        "Final result for %s recheck:  %s", self.metafile, self._result
-    )
-    return self._result
+    Parameters
+    ----------
+    txt : str
+        the preformated message to send to stdout.
+    """
+    termlen = shutil.get_terminal_size().columns
+    padding = " " * int(((termlen - len(txt)) / 2))
+    string = "".join(["\n", padding, txt, "\n"])
+    showtext(string)
 
+
@@ -18338,97 +26923,116 @@

-
+
-

-walk_file_tree(self, tree, partials) +

+showtext(txt) -

+

+
-

Traverse File Tree dictionary to get file details.

-

Extract full pathnames, length, root hash, and layer hashes -for each file included in the .torrent's file tree.

+

Print contents of txt to screen.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
treedict

File Tree dict extracted from torrent file.

required
+ - - - - + + + + - -
partialslist

list of intermediate pathnames.

requiredNameTypeDescriptionDefault
+ + + + txt + + str + +

text to print to terminal.

+ + required + + + + +
- Source code in torrentfile\recheck.py -
def walk_file_tree(self, tree: dict, partials: list):
+          Source code in torrentfile\interactive.py
+          
 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
def showtext(txt):
     """
-    Traverse File Tree dictionary to get file details.
-
-    Extract full pathnames, length, root hash, and layer hashes
-    for each file included in the .torrent's file tree.
+    Print contents of txt to screen.
 
     Parameters
     ----------
-    tree : dict
-        File Tree dict extracted from torrent file.
-    partials : list
-        list of intermediate pathnames.
+    txt : str
+        text to print to terminal.
     """
-    for key, val in tree.items():
-
-        # Empty string means the tree's leaf is value
-        if "" in val:
-
-            base = os.path.join(*partials, key)
-            roothash = None
-            length = val[""]["length"]
-            roothash = None if not length else val[""]["pieces root"]
-            full = str(self.root / base)
-            self.fileinfo[len(self.paths)] = {
-                "path": full,
-                "length": length,
-                "pieces root": roothash,
-            }
-            self.paths.append(full)
-            self.total += length
-        else:
-            self.walk_file_tree(val, partials + [key])
+    sys.stdout.write(txt)
 
+
-
+
+ + + + + +
+ +
+ +
+ + + +
+ + + +

+ torrentfile.recheck + + + +

+ +
+ +

Module container Checker Class.

+

The CheckerClass takes a torrentfile and tha path to it's contents. +It will then iterate through every file and directory contained +and compare their data to values contained within the torrent file. +Completion percentages will be printed to screen for each file and +at the end for the torrentfile as a whole.

+ + + +
+ -
-
-
@@ -18436,429 +27040,770 @@

-

- -FeedChecker (ProgMixin) - +

+ Checker

+
+

+ Bases: ProgMixin

-

Validates torrent content.

-

Seemlesly validate torrent file contents by comparing hashes in -metafile against data on disk.

-

Parameters:

- - - - - - - - - - - - - - - - +

Check a given file or directory to see if it matches a torrentfile.

+

Public constructor for Checker class instance.

+ +

Parameters:

+
NameTypeDescriptionDefault
checkerChecker

the checker class instance.

required
+ - - - - + + + + - -
hasherAny

hashing class for calculating piece hashes. default=None

requiredNameTypeDescriptionDefault
-
- Source code in torrentfile\recheck.py -
class FeedChecker(ProgMixin):
+    
+    
+        
+          metafile
+          
+                str
+          
+          

Path to ".torrent" file.

+ + required + + + + path + + str + +

Path where the content is located in filesystem.

+ + required + + + + +
Example
+
>> metafile = "/path/to/torrentfile/content_file_or_dir.torrent"
+>> location = "/path/to/location"
+>> os.path.exists("/path/to/location/content_file_or_dir")
+Out: True
+>> checker = Checker(metafile, location)
+
+ + +
+ Source code in torrentfile\recheck.py +
 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
class Checker(ProgMixin):
     """
-    Validates torrent content.
+    Check a given file or directory to see if it matches a torrentfile.
 
-    Seemlesly validate torrent file contents by comparing hashes in
-    metafile against data on disk.
+    Public constructor for Checker class instance.
 
     Parameters
     ----------
-    checker : object
-        the checker class instance.
-    hasher : Any
-        hashing class for calculating piece hashes. default=None
-    """
-
-    def __init__(self, checker: Checker):
-        """
-        Generate hashes of piece length data from filelist contents.
-        """
-        self.piece_length = checker.piece_length
-        self.paths = checker.paths
-        self.pieces = checker.info["pieces"]
-        self.fileinfo = checker.fileinfo
-        self.piece_map = {}
-        self.index = 0
-        self.piece_count = 0
-        self.it = None
-
-    def __iter__(self):
-        """
-        Assign iterator and return self.
-        """
-        self.it = self.iter_pieces()
-        return self
+    metafile : str
+        Path to ".torrent" file.
+    path : str
+        Path where the content is located in filesystem.
 
-    def __next__(self):
-        """
-        Yield back result of comparison.
-        """
-        try:
-            partial = next(self.it)
-        except StopIteration as itererror:
-            raise StopIteration from itererror
+    Example
+    -------
+        >> metafile = "/path/to/torrentfile/content_file_or_dir.torrent"
+        >> location = "/path/to/location"
+        >> os.path.exists("/path/to/location/content_file_or_dir")
+        Out: True
+        >> checker = Checker(metafile, location)
+    """
 
-        chunck = sha1(partial).digest()  # nosec
-        start = self.piece_count * SHA1
-        end = start + SHA1
-        piece = self.pieces[start:end]
-        self.piece_count += 1
-        path = self.paths[self.index]
-        return chunck, piece, path, len(partial)
+    _hook = None
 
-    def iter_pieces(self):
+    def __init__(self, metafile: str, path: str):
         """
-        Iterate through, and hash pieces of torrent contents.
+        Validate data against hashes contained in .torrent file.
 
-        Yields
-        ------
-        piece : bytes
-            hash digest for block of torrent data.
+        Parameters
+        ----------
+        metafile : str
+            path to .torrent file
+        path : str
+            path to content or contents parent directory.
         """
-        partial = bytearray()
-        for i, path in enumerate(self.paths):
-            total = self.fileinfo[i]["length"]
-            self.prog_start(total, path, unit="bytes")
-            self.index = i
-            if os.path.exists(path):
-                for piece in self.extract(path, partial):
-                    self.prog_update(len(piece))
-                    if (len(piece) == self.piece_length) or (
-                        i + 1 == len(self.paths)
-                    ):
-                        yield piece
-                    else:
-                        partial = piece
+        if not os.path.exists(metafile):
+            raise FileNotFoundError
+        meta = []
+        thread = Thread(target=pyben.loadinto, args=(metafile, meta))
+        thread.start()
+        self.last_log = None
+        self.log_msg("Checking: %s, %s", metafile, path)
+        self.metafile = metafile
+        self.total = 0
+        self.paths = []
+        self.fileinfo = {}
+        thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
+        if not meta:  # pragma: nocover
+            thread2.start()
+            thread2.join()
+        self.meta = meta[0]
+        self.info = self.meta["info"]
+        self.name = self.info["name"]
+        self.piece_length = self.info["piece length"]
 
+        if "meta version" in self.info:
+            if "pieces" in self.info:
+                self.meta_version = 3
             else:
-                length = self.fileinfo[i]["length"]
-                for pad in self._gen_padding(partial, length):
-                    if len(pad) == self.piece_length:
-                        yield pad
-                    else:
-                        partial = pad
-            self.prog_close()
+                self.meta_version = 2
+        else:
+            self.meta_version = 1
 
-    def extract(self, path: str, partial: bytearray) -> bytearray:
+        self.root = self.find_root(path)
+        self.check_paths()
+
+    @classmethod
+    def register_callback(cls, hook):
         """
-        Split file paths contents into blocks of data for hash pieces.
+        Register hooks from 3rd party programs to access generated info.
 
         Parameters
         ----------
-        path : str
-            path to content.
-        partial : bytes
-            any remaining content from last file.
+        hook : function
+            callback function for the logging feature.
+        """
+        cls._hook = hook
+
+    def piece_checker(self):
+        """
+        Check individual pieces of the torrent.
 
         Returns
         -------
-        bytearray
-            Hash digest for block of .torrent contents.
+        HashChecker | FeedChecker
+            Individual piece hasher.
         """
-        read = 0
-        length = self.fileinfo[self.index]["length"]
-        partial = bytearray() if len(partial) == self.piece_length else partial
-        if path not in self.paths:  # pragma: no cover
-            raise MissingPathError(path)
-        with open(path, "rb") as current:
-            while True:
-                bitlength = self.piece_length - len(partial)
-                part = bytearray(bitlength)
-                amount = current.readinto(part)
-                read += amount
-                partial.extend(part[:amount])
-                if amount < bitlength:
-                    if amount > 0 and read == length:
-                        self.prog_update(amount)
-                        yield partial
-                    break
-                self.prog_update(amount)
-                yield partial
-                partial = bytearray(0)
-        if length != read:
-            for pad in self._gen_padding(partial, length, read):
-                yield pad
+        if self.meta_version == 1:
+            return FeedChecker
+        return HashChecker
 
-    def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
+    def results(self):
         """
-        Create padded pieces where file sizes do not match.
+        Generate result percentage and store for future calls.
+        """
+        responses = []
+        for response in self.iter_hashes():
+            responses.append(response)
+
+        self.log_msg(
+            "Final result for %s recheck:  %s", self.metafile, self._result
+        )
+        return self._result
+
+    def log_msg(self, *args, level=logging.INFO):
+        """
+        Log message `msg` to logger and send `msg` to callback hook.
 
         Parameters
         ----------
-        partial : bytes
-            any remaining data from last file processed.
-        length : int
-            size of space that needs padding
-        read : int
-            portion of length already padded
-
-        Yields
-        ------
-        bytes
-            A piece length sized block of zeros.
+        args : dict
+            formatting args for log message
+        level : int
+            Log level for this message; default=`logging.INFO`
         """
-        while read < length:
-            left = self.piece_length - len(partial)
-            if length - read > left:
-                padding = bytearray(left)
-                partial.extend(padding)
-                yield partial
-                read += left
-                partial = bytearray(0)
-            else:
-                partial.extend(bytearray(length - read))
-                read = length
-                yield partial
-
- - + message = args[0] + if len(args) >= 3: + message = message % tuple(args[1:]) + elif len(args) == 2: + message = message % args[1] + # Repeat log messages should be ignored. + if message != self.last_log: + self.last_log = message + logger.log(level, message) + if self._hook and level == logging.INFO: + self._hook(message) -
+ def find_root(self, path: str) -> str: + """ + Check path for torrent content. + The path can be a relative or absolute filesystem path. In the case + where the content is a single file, the path may point directly to the + the file, or it may point to the parent directory. If content points + to a directory. The directory will be checked to see if it matches + the torrent's name, if not the directories contents will be searched. + The returned value will be the absolute path that matches the torrent's + name. + Parameters + ---------- + path : str + root path to torrent content + Returns + ------- + str + root path to content + """ + if not os.path.exists(path): + self.log_msg("Could not locate torrent content %s.", path) + raise FileNotFoundError(path) + root = Path(path) + if root.name == self.name: + self.log_msg("Content found: %s.", str(root)) + return root + if self.name in os.listdir(root): + return root / self.name + self.log_msg("Could not locate torrent content in: %s", str(root)) + raise FileNotFoundError(root) + def check_paths(self): + """ + Gather all file paths described in the torrent file. + """ + finfo = self.fileinfo + if "length" in self.info: + self.log_msg("%s points to a single file", self.root) + self.total = self.info["length"] + self.paths.append(str(self.root)) -
+ finfo[0] = { + "path": self.root, + "length": self.info["length"], + } + if self.meta_version > 1: + root = self.info["file tree"][self.name][""]["pieces root"] + finfo[0]["pieces root"] = root + return -

-__init__(self, checker) + # Otherwise Content is more than 1 file. + self.log_msg("%s points to a directory", self.root) + if self.meta_version == 1: - - special - + for i, item in enumerate(self.info["files"]): + self.total += item["length"] + base = os.path.join(*item["path"]) -

+ self.fileinfo[i] = { + "path": str(self.root / base), + "length": item["length"], + } -
+ self.paths.append(str(self.root / base)) + return -

Generate hashes of piece length data from filelist contents.

+ self.walk_file_tree(self.info["file tree"], []) -
- Source code in torrentfile\recheck.py -
def __init__(self, checker: Checker):
-    """
-    Generate hashes of piece length data from filelist contents.
-    """
-    self.piece_length = checker.piece_length
-    self.paths = checker.paths
-    self.pieces = checker.info["pieces"]
-    self.fileinfo = checker.fileinfo
-    self.piece_map = {}
-    self.index = 0
-    self.piece_count = 0
-    self.it = None
-
-
-
+ def walk_file_tree(self, tree: dict, partials: list): + """ + Traverse File Tree dictionary to get file details. -
+ Extract full pathnames, length, root hash, and layer hashes + for each file included in the .torrent's file tree. + Parameters + ---------- + tree : dict + File Tree dict extracted from torrent file. + partials : list + list of intermediate pathnames. + """ + for key, val in tree.items(): + # Empty string means the tree's leaf is value + if "" in val: -
+ base = os.path.join(*partials, key) + roothash = None + length = val[""]["length"] + roothash = None if not length else val[""]["pieces root"] + full = str(self.root / base) + self.fileinfo[len(self.paths)] = { + "path": full, + "length": length, + "pieces root": roothash, + } + self.paths.append(full) + self.total += length + else: + self.walk_file_tree(val, partials + [key]) + def iter_hashes(self) -> tuple: + """ + Produce results of comparing torrent contents piece by piece. + Yields + ------ + chunck : bytes + hash of data found on disk + piece : bytes + hash of data when complete and correct + path : str + path to file being hashed + size : int + length of bytes hashed for piece + """ + matched = consumed = 0 + checker = self.piece_checker() + for chunk, piece, path, size in checker(self): + consumed += size + matching = 0 + if chunk == piece: + matching += size + matched += size + yield chunk, piece, path, size + total_consumed = str(int(consumed / self.total * 100)) + percent_matched = str(int(matched / consumed * 100)) + self.log_msg( + "Processed: %s%%, Matched: %s%%", + total_consumed, + percent_matched, + level=logging.DEBUG, + ) + self._result = (matched / consumed) * 100 if consumed > 0 else 0 +
+
+
-

-__iter__(self) - - special - -

+
-
-

Assign iterator and return self.

-
- Source code in torrentfile\recheck.py -
def __iter__(self):
-    """
-    Assign iterator and return self.
-    """
-    self.it = self.iter_pieces()
-    return self
-
-
-
-
-
-

-__next__(self) - - special - -

-
-

Yield back result of comparison.

-
- Source code in torrentfile\recheck.py -
def __next__(self):
-    """
-    Yield back result of comparison.
-    """
-    try:
-        partial = next(self.it)
-    except StopIteration as itererror:
-        raise StopIteration from itererror
 
-    chunck = sha1(partial).digest()  # nosec
-    start = self.piece_count * SHA1
-    end = start + SHA1
-    piece = self.pieces[start:end]
-    self.piece_count += 1
-    path = self.paths[self.index]
-    return chunck, piece, path, len(partial)
-
-
-
-
-
+
-

-extract(self, path, partial) +

+__init__(metafile, path)

+
-

Split file paths contents into blocks of data for hash pieces.

+

Validate data against hashes contained in .torrent file.

-

Parameters:

- - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
pathstr

path to content.

required
+ - - - - + + + + - -
partialbytearray

any remaining content from last file.

requiredNameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytearray

Hash digest for block of .torrent contents.

+ + + + metafile + + str + +

path to .torrent file

+ + required + + + + path + + str + +

path to content or contents parent directory.

+ + required + + + + +
Source code in torrentfile\recheck.py -
def extract(self, path: str, partial: bytearray) -> bytearray:
+          
 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
def __init__(self, metafile: str, path: str):
     """
-    Split file paths contents into blocks of data for hash pieces.
+    Validate data against hashes contained in .torrent file.
 
     Parameters
     ----------
+    metafile : str
+        path to .torrent file
     path : str
-        path to content.
-    partial : bytes
-        any remaining content from last file.
-
-    Returns
-    -------
-    bytearray
-        Hash digest for block of .torrent contents.
+        path to content or contents parent directory.
     """
-    read = 0
-    length = self.fileinfo[self.index]["length"]
-    partial = bytearray() if len(partial) == self.piece_length else partial
-    if path not in self.paths:  # pragma: no cover
-        raise MissingPathError(path)
-    with open(path, "rb") as current:
-        while True:
-            bitlength = self.piece_length - len(partial)
-            part = bytearray(bitlength)
-            amount = current.readinto(part)
-            read += amount
-            partial.extend(part[:amount])
-            if amount < bitlength:
-                if amount > 0 and read == length:
-                    self.prog_update(amount)
-                    yield partial
-                break
-            self.prog_update(amount)
-            yield partial
-            partial = bytearray(0)
-    if length != read:
-        for pad in self._gen_padding(partial, length, read):
-            yield pad
+    if not os.path.exists(metafile):
+        raise FileNotFoundError
+    meta = []
+    thread = Thread(target=pyben.loadinto, args=(metafile, meta))
+    thread.start()
+    self.last_log = None
+    self.log_msg("Checking: %s, %s", metafile, path)
+    self.metafile = metafile
+    self.total = 0
+    self.paths = []
+    self.fileinfo = {}
+    thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
+    if not meta:  # pragma: nocover
+        thread2.start()
+        thread2.join()
+    self.meta = meta[0]
+    self.info = self.meta["info"]
+    self.name = self.info["name"]
+    self.piece_length = self.info["piece length"]
+
+    if "meta version" in self.info:
+        if "pieces" in self.info:
+            self.meta_version = 3
+        else:
+            self.meta_version = 2
+    else:
+        self.meta_version = 1
+
+    self.root = self.find_root(path)
+    self.check_paths()
 
+
@@ -18866,411 +27811,480 @@

-
+
-

-iter_pieces(self) +

+check_paths()

+
-

Iterate through, and hash pieces of torrent contents.

+

Gather all file paths described in the torrent file.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytes

hash digest for block of torrent data.

Source code in torrentfile\recheck.py -
def iter_pieces(self):
+          
213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
def check_paths(self):
     """
-    Iterate through, and hash pieces of torrent contents.
-
-    Yields
-    ------
-    piece : bytes
-        hash digest for block of torrent data.
+    Gather all file paths described in the torrent file.
     """
-    partial = bytearray()
-    for i, path in enumerate(self.paths):
-        total = self.fileinfo[i]["length"]
-        self.prog_start(total, path, unit="bytes")
-        self.index = i
-        if os.path.exists(path):
-            for piece in self.extract(path, partial):
-                self.prog_update(len(piece))
-                if (len(piece) == self.piece_length) or (
-                    i + 1 == len(self.paths)
-                ):
-                    yield piece
-                else:
-                    partial = piece
+    finfo = self.fileinfo
 
-        else:
-            length = self.fileinfo[i]["length"]
-            for pad in self._gen_padding(partial, length):
-                if len(pad) == self.piece_length:
-                    yield pad
-                else:
-                    partial = pad
-        self.prog_close()
-
- - + if "length" in self.info: + self.log_msg("%s points to a single file", self.root) + self.total = self.info["length"] + self.paths.append(str(self.root)) - + finfo[0] = { + "path": self.root, + "length": self.info["length"], + } + + if self.meta_version > 1: + root = self.info["file tree"][self.name][""]["pieces root"] + finfo[0]["pieces root"] = root + return + # Otherwise Content is more than 1 file. + self.log_msg("%s points to a directory", self.root) + if self.meta_version == 1: + for i, item in enumerate(self.info["files"]): + self.total += item["length"] + base = os.path.join(*item["path"]) + self.fileinfo[i] = { + "path": str(self.root / base), + "length": item["length"], + } - + self.paths.append(str(self.root / base)) + return + self.walk_file_tree(self.info["file tree"], []) + +
+
-
+
-

- -HashChecker (ProgMixin) - +

+find_root(path) +

-

-

Verify that root hashes of content files match the .torrent files.

+

Check path for torrent content.

+

The path can be a relative or absolute filesystem path. In the case +where the content is a single file, the path may point directly to the +the file, or it may point to the parent directory. If content points +to a directory. The directory will be checked to see if it matches +the torrent's name, if not the directories contents will be searched. +The returned value will be the absolute path that matches the torrent's +name.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + + + + + + + + + + +
checkerChecker

the checker instance that maintains variables.

requiredNameTypeDescriptionDefault
path + str +

root path to torrent content

+ required +
+ +

Returns:

+ + - - - - + + - -
hasherObject

the version specific hashing class for torrent content.

requiredTypeDescription
+ + + + + str + +

root path to content

+ + + +
Source code in torrentfile\recheck.py -
class HashChecker(ProgMixin):
+          
176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
def find_root(self, path: str) -> str:
     """
-    Verify that root hashes of content files match the .torrent files.
+    Check path for torrent content.
+
+    The path can be a relative or absolute filesystem path.  In the case
+    where the content is a single file, the path may point directly to the
+    the file, or it may point to the parent directory.  If content points
+    to a directory.  The directory will be checked to see if it matches
+    the torrent's name, if not the directories contents will be searched.
+    The returned value will be the absolute path that matches the torrent's
+    name.
 
     Parameters
     ----------
-    checker : Object
-        the checker instance that maintains variables.
-    hasher : Object
-        the version specific hashing class for torrent content.
-    """
-
-    def __init__(self, checker: Checker):
-        """
-        Construct a HybridChecker instance.
-        """
-        self.checker = checker
-        self.paths = checker.paths
-        self.piece_length = checker.piece_length
-        self.fileinfo = checker.fileinfo
-        self.piece_layers = checker.meta["piece layers"]
-        self.current = None
-        self.index = -1
-
-    def __iter__(self):
-        """
-        Assign iterator and return self.
-        """
-        return self
-
-    def __next__(self):
-        """
-        Provide the result of comparison.
-        """
-        if self.current is None:
-            self.next_file()
-        try:
-            return self.process_current()
-        except StopIteration as itererr:
-            if self.next_file():
-                return self.process_current()
-            raise StopIteration from itererr
-
-    class Padder:
-        """
-        Padding class to generate padding hashes wherever needed.
-
-        Parameters
-        ----------
-        length: int
-            the total size of the mock file generating padding for.
-        piece_length : int
-            the block size that each hash represents.
-        """
-
-        def __init__(self, length, piece_length):
-            """
-            Construct padding class to Mock missing or incomplete files.
-            """
-            self.length = length
-            self.piece_length = piece_length
-            self.pad = sha256(bytearray(piece_length)).digest()
-
-        def __iter__(self):
-            """
-            Return self to correctly implement iterator type.
-            """
-            return self  # pragma: nocover
+    path : str
+        root path to torrent content
 
-        def __next__(self):
-            """
-            Iterate through seemingly endless sha256 hashes of zeros.
-            """
-            if self.length >= self.piece_length:
-                self.length -= self.piece_length
-                return self.pad
-            if self.length > 0:
-                pad = sha256(bytearray(self.length)).digest()
-                self.length -= self.length
-                return pad
-            raise StopIteration
+    Returns
+    -------
+    str
+        root path to content
+    """
+    if not os.path.exists(path):
+        self.log_msg("Could not locate torrent content %s.", path)
+        raise FileNotFoundError(path)
 
-    def next_file(self):
-        """
-        Remove all references to  processed files and prepare for the next.
-        """
-        self.index += 1
-        self.prog_close()
-        if self.current is None or self.index < len(self.paths):
-            self.current = self.paths[self.index]
-            self.length = self.fileinfo[self.index]["length"]
-            self.root_hash = self.fileinfo[self.index]["pieces root"]
-            if self.length > self.piece_length:
-                self.pieces = self.piece_layers[self.root_hash]
-            else:
-                self.pieces = self.root_hash
-            path = self.paths[self.index]
-            self.prog_start(self.length, path, unit="bytes")
-            self.count = 0
-            if os.path.exists(self.current):
-                self.hasher = FileHasher(path, self.piece_length, progress=0)
-            else:
-                self.hasher = self.Padder(self.length, self.piece_length)
-            return True
-        if self.index >= len(self.paths):
-            del self.current
-            del self.length
-            del self.root_hash
-            del self.pieces
-        return False
+    root = Path(path)
+    if root.name == self.name:
+        self.log_msg("Content found: %s.", str(root))
+        return root
 
-    def process_current(self):
-        """
-        Gather necessary information to compare to metafile details.
-        """
-        try:
-            layer = next(self.hasher)
-            piece, size = self.advance()
-            self.prog_update(size)
-            return layer, piece, self.current, size
-        except StopIteration as err:
-            if self.length > 0 and self.count * SHA256 < len(self.pieces):
-                self.hasher = self.Padder(self.length, self.piece_length)
-                piece, size = self.advance()
-                layer = next(self.hasher)
-                self.prog_update(0)
-                return layer, piece, self.current, size
-            raise StopIteration from err
+    if self.name in os.listdir(root):
+        return root / self.name
 
-    def advance(self):
-        """
-        Increment the number of pieces processed for the current file.
-        """
-        start = self.count * SHA256
-        end = start + SHA256
-        piece = self.pieces[start:end]
-        self.count += 1
-        if self.length >= self.piece_length:
-            self.length -= self.piece_length
-            size = self.piece_length
-        else:
-            size = self.length
-            self.length -= self.length
-        return piece, size
+    self.log_msg("Could not locate torrent content in: %s", str(root))
+    raise FileNotFoundError(root)
 
+
+
+
-
- - - - - - - -
+
-

- -Padder +

+iter_hashes()

+
-

Padding class to generate padding hashes wherever needed.

+

Produce results of comparing torrent contents piece by piece.

-

Parameters:

- - - - - - - - - - - - - - - - +

Yields:

+
NameTypeDescriptionDefault
lengthint

the total size of the mock file generating padding for.

required
+ - - - - + + - -
piece_lengthint

the block size that each hash represents.

requiredName TypeDescription
+ + + +chunck + bytes + +

hash of data found on disk

+ + +piece + bytes + +

hash of data when complete and correct

+ + +path + str + +

path to file being hashed

+ + +size + int + +

length of bytes hashed for piece

+ + + +
Source code in torrentfile\recheck.py -
class Padder:
+          
287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
def iter_hashes(self) -> tuple:
     """
-    Padding class to generate padding hashes wherever needed.
+    Produce results of comparing torrent contents piece by piece.
 
-    Parameters
-    ----------
-    length: int
-        the total size of the mock file generating padding for.
-    piece_length : int
-        the block size that each hash represents.
+    Yields
+    ------
+    chunck : bytes
+        hash of data found on disk
+    piece : bytes
+        hash of data when complete and correct
+    path : str
+        path to file being hashed
+    size : int
+        length of bytes hashed for piece
     """
-
-    def __init__(self, length, piece_length):
-        """
-        Construct padding class to Mock missing or incomplete files.
-        """
-        self.length = length
-        self.piece_length = piece_length
-        self.pad = sha256(bytearray(piece_length)).digest()
-
-    def __iter__(self):
-        """
-        Return self to correctly implement iterator type.
-        """
-        return self  # pragma: nocover
-
-    def __next__(self):
-        """
-        Iterate through seemingly endless sha256 hashes of zeros.
-        """
-        if self.length >= self.piece_length:
-            self.length -= self.piece_length
-            return self.pad
-        if self.length > 0:
-            pad = sha256(bytearray(self.length)).digest()
-            self.length -= self.length
-            return pad
-        raise StopIteration
+    matched = consumed = 0
+    checker = self.piece_checker()
+    for chunk, piece, path, size in checker(self):
+        consumed += size
+        matching = 0
+        if chunk == piece:
+            matching += size
+            matched += size
+        yield chunk, piece, path, size
+        total_consumed = str(int(consumed / self.total * 100))
+        percent_matched = str(int(matched / consumed * 100))
+        self.log_msg(
+            "Processed: %s%%, Matched: %s%%",
+            total_consumed,
+            percent_matched,
+            level=logging.DEBUG,
+        )
+    self._result = (matched / consumed) * 100 if consumed > 0 else 0
 
+
+
- - -
- - - - - +
+
-
+

+log_msg(*args, level=logging.INFO) -

-__init__(self, length, piece_length) - - special - +
-
-

Construct padding class to Mock missing or incomplete files.

+

Log message msg to logger and send msg to callback hook.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
args + dict +

formatting args for log message

+ required +
level + int +

Log level for this message; default=logging.INFO

+ logging.INFO +
Source code in torrentfile\recheck.py -
def __init__(self, length, piece_length):
+          
152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
def log_msg(self, *args, level=logging.INFO):
     """
-    Construct padding class to Mock missing or incomplete files.
+    Log message `msg` to logger and send `msg` to callback hook.
+
+    Parameters
+    ----------
+    args : dict
+        formatting args for log message
+    level : int
+        Log level for this message; default=`logging.INFO`
     """
-    self.length = length
-    self.piece_length = piece_length
-    self.pad = sha256(bytearray(piece_length)).digest()
+    message = args[0]
+    if len(args) >= 3:
+        message = message % tuple(args[1:])
+    elif len(args) == 2:
+        message = message % args[1]
+
+    # Repeat log messages should be ignored.
+    if message != self.last_log:
+        self.last_log = message
+        logger.log(level, message)
+        if self._hook and level == logging.INFO:
+            self._hook(message)
 
+
@@ -19278,31 +28292,66 @@
+
-
-__iter__(self) +

+piece_checker() - - special - -

+
+
-

Return self to correctly implement iterator type.

+

Check individual pieces of the torrent.

+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ HashChecker | FeedChecker +

Individual piece hasher.

Source code in torrentfile\recheck.py -
def __iter__(self):
+          
126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
def piece_checker(self):
     """
-    Return self to correctly implement iterator type.
+    Check individual pieces of the torrent.
+
+    Returns
+    -------
+    HashChecker | FeedChecker
+        Individual piece hasher.
     """
-    return self  # pragma: nocover
+    if self.meta_version == 1:
+        return FeedChecker
+    return HashChecker
 
+
@@ -19310,38 +28359,73 @@
+
-
-__next__(self) +

+register_callback(hook) - special + classmethod -

+
+
-

Iterate through seemingly endless sha256 hashes of zeros.

+

Register hooks from 3rd party programs to access generated info.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
hook + function +

callback function for the logging feature.

+ required +
Source code in torrentfile\recheck.py -
def __next__(self):
+          
114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
@classmethod
+def register_callback(cls, hook):
     """
-    Iterate through seemingly endless sha256 hashes of zeros.
+    Register hooks from 3rd party programs to access generated info.
+
+    Parameters
+    ----------
+    hook : function
+        callback function for the logging feature.
     """
-    if self.length >= self.piece_length:
-        self.length -= self.piece_length
-        return self.pad
-    if self.length > 0:
-        pad = sha256(bytearray(self.length)).digest()
-        self.length -= self.length
-        return pad
-    raise StopIteration
+    cls._hook = hook
 
+
@@ -19349,49 +28433,48 @@
- -
- -
- - - - - -
+
-

-__init__(self, checker) +

+results() - - special -

+
-

Construct a HybridChecker instance.

+

Generate result percentage and store for future calls.

Source code in torrentfile\recheck.py -
def __init__(self, checker: Checker):
+          
139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
def results(self):
     """
-    Construct a HybridChecker instance.
+    Generate result percentage and store for future calls.
     """
-    self.checker = checker
-    self.paths = checker.paths
-    self.piece_length = checker.piece_length
-    self.fileinfo = checker.fileinfo
-    self.piece_layers = checker.meta["piece layers"]
-    self.current = None
-    self.index = -1
+    responses = []
+    for response in self.iter_hashes():
+        responses.append(response)
+
+    self.log_msg(
+        "Final result for %s recheck:  %s", self.metafile, self._result
+    )
+    return self._result
 
+
@@ -19399,31 +28482,126 @@

-
+
-

-__iter__(self) +

+walk_file_tree(tree, partials) - - special -

+
-

Assign iterator and return self.

+

Traverse File Tree dictionary to get file details.

+

Extract full pathnames, length, root hash, and layer hashes +for each file included in the .torrent's file tree.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
tree + dict +

File Tree dict extracted from torrent file.

+ required +
partials + list +

list of intermediate pathnames.

+ required +
Source code in torrentfile\recheck.py -
def __iter__(self):
+          
253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
def walk_file_tree(self, tree: dict, partials: list):
     """
-    Assign iterator and return self.
+    Traverse File Tree dictionary to get file details.
+
+    Extract full pathnames, length, root hash, and layer hashes
+    for each file included in the .torrent's file tree.
+
+    Parameters
+    ----------
+    tree : dict
+        File Tree dict extracted from torrent file.
+    partials : list
+        list of intermediate pathnames.
     """
-    return self
+    for key, val in tree.items():
+
+        # Empty string means the tree's leaf is value
+        if "" in val:
+
+            base = os.path.join(*partials, key)
+            roothash = None
+            length = val[""]["length"]
+            roothash = None if not length else val[""]["pieces root"]
+            full = str(self.root / base)
+            self.fileinfo[len(self.paths)] = {
+                "path": full,
+                "length": length,
+                "pieces root": roothash,
+            }
+            self.paths.append(full)
+            self.total += length
+        else:
+            self.walk_file_tree(val, partials + [key])
 
+
@@ -19431,1507 +28609,1940 @@

-
- - - -

-__next__(self) - - - special - - -

-
-

Provide the result of comparison.

+
-
- Source code in torrentfile\recheck.py -
def __next__(self):
-    """
-    Provide the result of comparison.
-    """
-    if self.current is None:
-        self.next_file()
-    try:
-        return self.process_current()
-    except StopIteration as itererr:
-        if self.next_file():
-            return self.process_current()
-        raise StopIteration from itererr
-
-

-
+
-

-advance(self) +

+ FeedChecker -

+ +

+
+

+ Bases: ProgMixin

-

Increment the number of pieces processed for the current file.

-
- Source code in torrentfile\recheck.py -
def advance(self):
+      

Validates torrent content.

+

Seemlesly validate torrent file contents by comparing hashes in +metafile against data on disk.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
checker + object +

the checker class instance.

+ required +
hasher + Any +

hashing class for calculating piece hashes. default=None

+ required +
+ + +
+ Source code in torrentfile\recheck.py +
322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
class FeedChecker(ProgMixin):
     """
-    Increment the number of pieces processed for the current file.
+    Validates torrent content.
+
+    Seemlesly validate torrent file contents by comparing hashes in
+    metafile against data on disk.
+
+    Parameters
+    ----------
+    checker : object
+        the checker class instance.
+    hasher : Any
+        hashing class for calculating piece hashes. default=None
     """
-    start = self.count * SHA256
-    end = start + SHA256
-    piece = self.pieces[start:end]
-    self.count += 1
-    if self.length >= self.piece_length:
-        self.length -= self.piece_length
-        size = self.piece_length
-    else:
-        size = self.length
-        self.length -= self.length
-    return piece, size
-
- - - + def __init__(self, checker: Checker): + """ + Generate hashes of piece length data from filelist contents. + """ + self.piece_length = checker.piece_length + self.paths = checker.paths + self.pieces = checker.info["pieces"] + self.fileinfo = checker.fileinfo + self.piece_map = {} + self.index = 0 + self.piece_count = 0 + self.it = None + def __iter__(self): + """ + Assign iterator and return self. + """ + self.it = self.iter_pieces() + return self + def __next__(self): + """ + Yield back result of comparison. + """ + try: + partial = next(self.it) + except StopIteration as itererror: + raise StopIteration from itererror -
+ chunck = sha1(partial).digest() # nosec + start = self.piece_count * SHA1 + end = start + SHA1 + piece = self.pieces[start:end] + self.piece_count += 1 + path = self.paths[self.index] + return chunck, piece, path, len(partial) + def iter_pieces(self): + """ + Iterate through, and hash pieces of torrent contents. + Yields + ------ + piece : bytes + hash digest for block of torrent data. + """ + partial = bytearray() + for i, path in enumerate(self.paths): + total = self.fileinfo[i]["length"] + self.prog_start(total, path, unit="bytes") + self.index = i + if os.path.exists(path): + for piece in self.extract(path, partial): + self.prog_update(len(piece)) + if (len(piece) == self.piece_length) or ( + i + 1 == len(self.paths) + ): + yield piece + else: + partial = piece -

-next_file(self) + else: + length = self.fileinfo[i]["length"] + for pad in self._gen_padding(partial, length): + if len(pad) == self.piece_length: + yield pad + else: + partial = pad + self.prog_close() + def extract(self, path: str, partial: bytearray) -> bytearray: + """ + Split file paths contents into blocks of data for hash pieces. -

+ Parameters + ---------- + path : str + path to content. + partial : bytes + any remaining content from last file. -
+ Returns + ------- + bytearray + Hash digest for block of .torrent contents. + """ + read = 0 + length = self.fileinfo[self.index]["length"] + partial = bytearray() if len(partial) == self.piece_length else partial + if path not in self.paths: # pragma: no cover + raise MissingPathError(path) + with open(path, "rb") as current: + while True: + bitlength = self.piece_length - len(partial) + part = bytearray(bitlength) + amount = current.readinto(part) + read += amount + partial.extend(part[:amount]) + if amount < bitlength: + if amount > 0 and read == length: + self.prog_update(amount) + yield partial + break + self.prog_update(amount) + yield partial + partial = bytearray(0) + if length != read: + for pad in self._gen_padding(partial, length, read): + yield pad -

Remove all references to processed files and prepare for the next.

+ def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes: + """ + Create padded pieces where file sizes do not match. -
- Source code in torrentfile\recheck.py -
def next_file(self):
-    """
-    Remove all references to  processed files and prepare for the next.
-    """
-    self.index += 1
-    self.prog_close()
-    if self.current is None or self.index < len(self.paths):
-        self.current = self.paths[self.index]
-        self.length = self.fileinfo[self.index]["length"]
-        self.root_hash = self.fileinfo[self.index]["pieces root"]
-        if self.length > self.piece_length:
-            self.pieces = self.piece_layers[self.root_hash]
-        else:
-            self.pieces = self.root_hash
-        path = self.paths[self.index]
-        self.prog_start(self.length, path, unit="bytes")
-        self.count = 0
-        if os.path.exists(self.current):
-            self.hasher = FileHasher(path, self.piece_length, progress=0)
-        else:
-            self.hasher = self.Padder(self.length, self.piece_length)
-        return True
-    if self.index >= len(self.paths):
-        del self.current
-        del self.length
-        del self.root_hash
-        del self.pieces
-    return False
+        Parameters
+        ----------
+        partial : bytes
+            any remaining data from last file processed.
+        length : int
+            size of space that needs padding
+        read : int
+            portion of length already padded
+
+        Yields
+        ------
+        bytes
+            A piece length sized block of zeros.
+        """
+        while read < length:
+            left = self.piece_length - len(partial)
+            if length - read > left:
+                padding = bytearray(left)
+                partial.extend(padding)
+                yield partial
+                read += left
+                partial = bytearray(0)
+            else:
+                partial.extend(bytearray(length - read))
+                read = length
+                yield partial
 
-
-
+
+
-
+
-
-

-process_current(self) -

-
-

Gather necessary information to compare to metafile details.

-
- Source code in torrentfile\recheck.py -
def process_current(self):
-    """
-    Gather necessary information to compare to metafile details.
-    """
-    try:
-        layer = next(self.hasher)
-        piece, size = self.advance()
-        self.prog_update(size)
-        return layer, piece, self.current, size
-    except StopIteration as err:
-        if self.length > 0 and self.count * SHA256 < len(self.pieces):
-            self.hasher = self.Padder(self.length, self.piece_length)
-            piece, size = self.advance()
-            layer = next(self.hasher)
-            self.prog_update(0)
-            return layer, piece, self.current, size
-        raise StopIteration from err
-
-
-
-
-
-
+
-
+

+__init__(checker) +

+
-
+

Generate hashes of piece length data from filelist contents.

+
+ Source code in torrentfile\recheck.py +
337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
def __init__(self, checker: Checker):
+    """
+    Generate hashes of piece length data from filelist contents.
+    """
+    self.piece_length = checker.piece_length
+    self.paths = checker.paths
+    self.pieces = checker.info["pieces"]
+    self.fileinfo = checker.fileinfo
+    self.piece_map = {}
+    self.index = 0
+    self.piece_count = 0
+    self.it = None
+
+
+
-
+
-

- torrentfile.torrent +

+__iter__() +

- -
+
-

Classes and procedures pertaining to the creation of torrent meta files.

-

Classes

-
    -
  • -

    TorrentFile - construct .torrent file.

    -
  • -
  • -

    TorrentFileV2 - construct .torrent v2 files using provided data.

    -
  • -
  • -

    MetaFile - base class for all MetaFile classes.

    -
  • -
-

Constants

-
    -
  • -

    BLOCK_SIZE : int - size of leaf hashes for merkle tree.

    -
  • -
  • -

    HASH_SIZE : int - Length of a sha256 hash.

    -
  • -
-

Bittorrent V2

-

From Bittorrent.org Documentation pages.

-

Implementation details for Bittorrent Protocol v2.

-
-

Note

-

All strings in a .torrent file that contain text must be UTF-8 encoded.

-
-
Meta Version 2 Dictionary:
-
    -
  • -

    "announce": - The URL of the tracker.

    -
  • -
  • -

    "info": - This maps to a dictionary, with keys described below.

    -
      -
    • -

      "name": - A display name for the torrent. It is purely advisory.

      -
    • -
    • -

      "piece length": - The number of bytes that each logical piece in the peer - protocol refers to. I.e. it sets the granularity of piece, request, - bitfield and have messages. It must be a power of two and at least - 6KiB.

      -
    • -
    • -

      "meta version": - An integer value, set to 2 to indicate compatibility - with the current revision of this specification. Version 1 is not - assigned to avoid confusion with BEP3. Future revisions will only - increment this issue to indicate an incompatible change has been made, - for example that hash algorithms were changed due to newly discovered - vulnerabilities. Lementations must check this field first and indicate - that a torrent is of a newer version than they can handle before - performing other idations which may result in more general messages - about invalid files. Files are mapped into this piece address space so - that each non-empty

      -
    • -
    • -

      "file tree": - A tree of dictionaries where dictionary keys represent UTF-8 - encoded path elements. Entries with zero-length keys describe the - properties of the composed path at that point. 'UTF-8 encoded' - context only means that if the native encoding is known at creation - time it must be converted to UTF-8. Keys may contain invalid UTF-8 - sequences or characters and names that are reserved on specific - filesystems. Implementations must be prepared to sanitize them. On - platforms path components exactly matching '.' and '..' must be - sanitized since they could lead to directory traversal attacks and - conflicting path descriptions. On platforms that require UTF-8 - path components this sanitizing step must happen after normalizing - overlong UTF-8 encodings. - File is aligned to a piece boundary and occurs in same order as - the file tree. The last piece of each file may be shorter than the - specified piece length, resulting in an alignment gap.

      -
    • -
    • -

      "length": - Length of the file in bytes. Presence of this field indicates - that the dictionary describes a file, not a directory. Which means - it must not have any sibling entries.

      -
    • -
    • -

      "pieces root": - For non-empty files this is the the root hash of a merkle - tree with a branching factor of 2, constructed from 16KiB blocks - of the file. The last block may be shorter than 16KiB. The - remaining leaf hashes beyond the end of the file required to - construct upper layers of the merkle tree are set to zero. As of - meta version 2 SHA2-256 is used as digest function for the merkle - tree. The hash is stored in its binary form, not as human-readable - string.

      -
    • -
    -
  • -
  • -

    "piece layers": - A dictionary of strings. For each file in the file tree that - is larger than the piece size it contains one string value. - The keys are the merkle roots while the values consist of concatenated - hashes of one layer within that merkle tree. The layer is chosen so - that one hash covers piece length bytes. For example if the piece - size is 16KiB then the leaf hashes are used. If a piece size of - 128KiB is used then 3rd layer up from the leaf hashes is used. Layer - hashes which exclusively cover data beyond the end of file, i.e. - are only needed to balance the tree, are omitted. All hashes are - stored in their binary format. A torrent is not valid if this field is - absent, the contained hashes do not match the merkle roots or are - not from the correct layer.

    -
  • -
-
-

Important

-

The file tree root dictionary itself must not be a file, -i.e. it must not contain a zero-length key with a dictionary containing -a length key.

-
-

Bittorrent V1

-
v1 meta-dictionary
-
    -
  • -

    announce: - The URL of the tracker.

    -
  • -
  • -

    info: - This maps to a dictionary, with keys described below.

    -
      -
    • -

      name: - maps to a UTF-8 encoded string which is the suggested name to - save the file (or directory) as. It is purely advisory.

      -
    • -
    • -

      piece length: - maps to the number of bytes in each piece the file is split - into. For the purposes of transfer, files are split into - fixed-size pieces which are all the same length except for - possibly the last one which may be truncated.

      -
    • -
    • -

      piece length: - is almost always a power of two, most commonly 2^18 = 256 K

      -
    • -
    • -

      pieces: - maps to a string whose length is a multiple of 20. It is to be - subdivided into strings of length 20, each of which is the SHA1 - hash of the piece at the corresponding index.

      -
    • -
    • -

      length: - In the single file case, maps to the length of the file in bytes.

      -
    • -
    • -

      files: - If present then the download represents a single file, otherwise it - represents a set of files which go in a directory structure. - For the purposes of the other keys, the multi-file case is treated - as only having a single file by concatenating the files in the order - they appear in the files list. The files list is the value files - maps to, and is a list of dictionaries containing the following keys:

      -
        -
      • -

        path: - A list of UTF-8 encoded strings corresponding to subdirectory - names, the last of which is the actual file name

        -
      • -
      • -

        length: - Maps to the length of the file in bytes.

        -
      • -
      -
    • -
    • -

      length: - Only present if the content is a single file. Maps to the length - of the file in bytes.

      -
    • -
    -
  • -
-
-

Note

-

In the single file case, the name key is the name of a file, -in the muliple file case, it's the name of a directory.

-
+

Assign iterator and return self.

+ +
+ Source code in torrentfile\recheck.py +
350
+351
+352
+353
+354
+355
def __iter__(self):
+    """
+    Assign iterator and return self.
+    """
+    self.it = self.iter_pieces()
+    return self
+
+
+
+
+
+ + + +
-
+

+__next__() + + +

+
+ +

Yield back result of comparison.

+ +
+ Source code in torrentfile\recheck.py +
357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
def __next__(self):
+    """
+    Yield back result of comparison.
+    """
+    try:
+        partial = next(self.it)
+    except StopIteration as itererror:
+        raise StopIteration from itererror
 
+    chunck = sha1(partial).digest()  # nosec
+    start = self.piece_count * SHA1
+    end = start + SHA1
+    piece = self.pieces[start:end]
+    self.piece_count += 1
+    path = self.paths[self.index]
+    return chunck, piece, path, len(partial)
+
+
+
+
+
-
+
-

- -MetaFile +

+_gen_padding(partial, length, read=0) +

-
-

Base Class for all TorrentFile classes.

+

Create padded pieces where file sizes do not match.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
pathstr

target path to torrent content. Default: None

None
announcestr

One or more tracker URL's. Default: None

None
commentstr

A comment. Default: None

None
+ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + +
piece_lengthint

Size of torrent pieces. Default: None

NoneNameTypeDescriptionDefault
partial + bytes +

any remaining data from last file processed.

+ required +
length + int +

size of space that needs padding

+ required +
read + int +

portion of length already padded

+ 0 +
+ +

Yields:

+ + - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
privatebool

For private trackers. Default: None

FalseTypeDescription
outfilestr

target path to write .torrent file. Default: None

None
sourcestr

Private tracker source. Default: None

None
progressstr

level of progress bar displayed Default: "1"

1
cwdbool

If True change default save location to current directory

False
httpseedslist

one or more web addresses where torrent content can be found.

None
url_listlist

one or more web addressess where torrent content exists.

None
contentstr

alias for 'path' arg.

None
meta_versionint

indicates which Bittorrent protocol to use for hashing content

None
+ + + + + bytes + +

A piece length sized block of zeros.

+ + + +
- Source code in torrentfile\torrent.py -
class MetaFile:
-    """
-    Base Class for all TorrentFile classes.
+          Source code in torrentfile\recheck.py
+          
447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
+    """
+    Create padded pieces where file sizes do not match.
 
     Parameters
     ----------
-    path : str
-        target path to torrent content.  Default: None
-    announce : str
-        One or more tracker URL's.  Default: None
-    comment : str
-        A comment.  Default: None
-    piece_length : int
-        Size of torrent pieces.  Default: None
-    private : bool
-        For private trackers.  Default: None
-    outfile : str
-        target path to write .torrent file. Default: None
-    source : str
-        Private tracker source. Default: None
-    progress : str
-        level of progress bar displayed  Default: "1"
-    cwd : bool
-        If True change default save location to current directory
-    httpseeds : list
-        one or more web addresses where torrent content can be found.
-    url_list : list
-        one or more web addressess where torrent content exists.
-    content : str
-        alias for 'path' arg.
-    meta_version : int
-        indicates which Bittorrent protocol to use for hashing content
-    """
-
-    hasher = None
-
-    @classmethod
-    def set_callback(cls, func):
-        """
-        Assign a callback function for the Hashing class to call for each hash.
-
-        Parameters
-        ----------
-        func : function
-            The callback function which accepts a single paramter.
-        """
-        if "hasher" in vars(cls) and vars(cls)["hasher"]:
-            cls.hasher.set_callback(func)
-
-    def __init__(
-        self,
-        path=None,
-        announce=None,
-        comment=None,
-        piece_length=None,
-        private=False,
-        outfile=None,
-        source=None,
-        progress=1,
-        cwd=False,
-        httpseeds=None,
-        url_list=None,
-        content=None,
-        meta_version=None,
-        **_,
-    ):
-        """
-        Construct MetaFile superclass and assign local attributes.
-        """
-        self.private = private
-        self.cwd = cwd
-        self.outfile = outfile
-        self.progress = int(progress)
-        self.comment = comment
-        self.source = source
-        self.meta_version = meta_version
-
-        if content:
-            path = content
-        if not path:
-            if announce and len(announce) > 1 and os.path.exists(announce[-1]):
-                path = announce[-1]
-                announce = announce[:-1]
-            elif url_list and os.path.exists(url_list[-1]):
-                path = url_list[-1]
-                url_list = url_list[:-1]
-            elif httpseeds and os.path.exists(httpseeds[-1]):
-                path = httpseeds[-1]
-                httpseeds = httpseeds[:-1]
-            else:
-                raise utils.MissingPathError("Path to content is required.")
-
-        # base path to torrent content.
-        self.path = path
-
-        logger.debug("path parameter found %s", path)
+    partial : bytes
+        any remaining data from last file processed.
+    length : int
+        size of space that needs padding
+    read : int
+        portion of length already padded
 
-        # Format piece_length attribute.
-        if piece_length:
-            self.piece_length = utils.normalize_piece_length(piece_length)
-            logger.debug("piece length parameter found %s", piece_length)
+    Yields
+    ------
+    bytes
+        A piece length sized block of zeros.
+    """
+    while read < length:
+        left = self.piece_length - len(partial)
+        if length - read > left:
+            padding = bytearray(left)
+            partial.extend(padding)
+            yield partial
+            read += left
+            partial = bytearray(0)
         else:
-            self.piece_length = utils.path_piece_length(self.path)
-            logger.debug("piece length calculated %s", self.piece_length)
-
-        # Assign announce URL to empty string if none provided.
-        if not announce:
-            self.announce, self.announce_list = "", [[""]]
-
-        # Most torrent clients have editting trackers as a feature.
-        elif isinstance(announce, str):
-            self.announce, self.announce_list = announce, [[announce]]
-
-        elif isinstance(announce, Sequence):
-            self.announce, self.announce_list = announce[0], [announce]
-
-        self.meta = {
-            "announce": self.announce,
-            "announce-list": self.announce_list,
-            "created by": f"TorrentFile:v{version}",
-            "creation date": int(datetime.timestamp(datetime.now())),
-            "info": {},
-        }
-        if comment:
-            self.meta["info"]["comment"] = comment
-            logger.debug("comment parameter found %s", comment)
-        if private:
-            self.meta["info"]["private"] = 1
-            logger.debug("private parameter triggered")
-        if source:
-            self.meta["info"]["source"] = source
-            logger.debug("source parameter found %s", source)
-        if url_list:
-            self.meta["url-list"] = url_list
-            logger.debug("url list parameter found %s", str(url_list))
-        if httpseeds:
-            self.meta["httpseeds"] = httpseeds
-            logger.debug("httpseeds parameter found %s", str(httpseeds))
-        self.meta["info"]["piece length"] = self.piece_length
-
-        self.meta_version = meta_version
-        parent, self.name = os.path.split(self.path)
-        if not self.name:
-            self.name = os.path.basename(parent)
-        self.meta["info"]["name"] = self.name
-
-    def assemble(self):
-        """
-        Overload in subclasses.
-
-        Raises
-        ------
-        Exception
-            NotImplementedError
-        """
-        raise NotImplementedError
+            partial.extend(bytearray(length - read))
+            read = length
+            yield partial
+
+
+
+
- def sort_meta(self): - """Sort the info and meta dictionaries.""" - logger.debug("sorting dictionary keys") - meta = self.meta - meta["info"] = dict(sorted(list(meta["info"].items()))) - meta = dict(sorted(list(meta.items()))) - return meta +
- def write(self, outfile=None) -> tuple: - """ - Write meta information to .torrent file. - Parameters - ---------- - outfile : str - Destination path for .torrent file. default=None - Returns - ------- - outfile : str - Where the .torrent file was writen. - meta : dict - .torrent meta information. - """ - fallback = os.path.join(os.getcwd(), self.name) + ".torrent" - if not self.outfile and not outfile: - if self.cwd: - self.outfile = fallback - else: - path = str(self.path).rstrip("\\/") - self.outfile = path + ".torrent" - elif outfile: - self.outfile = outfile - if str(self.outfile)[-1] in "\\/": - self.outfile = self.outfile + (self.name + ".torrent") - self.meta = self.sort_meta() - try: - pyben.dump(self.meta, self.outfile) - except PermissionError: - self.outfile = fallback - pyben.dump(self.meta, fallback) - return self.outfile, self.meta -
-
+
-
+

+extract(path, partial) + + +

+ +
+

Split file paths contents into blocks of data for hash pieces.

+

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

path to content.

+ required +
partial + bytes +

any remaining content from last file.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ bytearray +

Hash digest for block of .torrent contents.

+
+ Source code in torrentfile\recheck.py +
407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
def extract(self, path: str, partial: bytearray) -> bytearray:
+    """
+    Split file paths contents into blocks of data for hash pieces.
 
+    Parameters
+    ----------
+    path : str
+        path to content.
+    partial : bytes
+        any remaining content from last file.
 
+    Returns
+    -------
+    bytearray
+        Hash digest for block of .torrent contents.
+    """
+    read = 0
+    length = self.fileinfo[self.index]["length"]
+    partial = bytearray() if len(partial) == self.piece_length else partial
+    if path not in self.paths:  # pragma: no cover
+        raise MissingPathError(path)
+    with open(path, "rb") as current:
+        while True:
+            bitlength = self.piece_length - len(partial)
+            part = bytearray(bitlength)
+            amount = current.readinto(part)
+            read += amount
+            partial.extend(part[:amount])
+            if amount < bitlength:
+                if amount > 0 and read == length:
+                    self.prog_update(amount)
+                    yield partial
+                break
+            self.prog_update(amount)
+            yield partial
+            partial = bytearray(0)
+    if length != read:
+        for pad in self._gen_padding(partial, length, read):
+            yield pad
+
+
+
+
+
-
+
-

-__init__(self, path=None, announce=None, comment=None, piece_length=None, private=False, outfile=None, source=None, progress=1, cwd=False, httpseeds=None, url_list=None, content=None, meta_version=None, **_) +

+iter_pieces() - - special -

+
-

Construct MetaFile superclass and assign local attributes.

+

Iterate through, and hash pieces of torrent contents.

+ +

Yields:

+ + + + + + + + + + + + + +
Name TypeDescription
piece + bytes +

hash digest for block of torrent data.

- Source code in torrentfile\torrent.py -
def __init__(
-    self,
-    path=None,
-    announce=None,
-    comment=None,
-    piece_length=None,
-    private=False,
-    outfile=None,
-    source=None,
-    progress=1,
-    cwd=False,
-    httpseeds=None,
-    url_list=None,
-    content=None,
-    meta_version=None,
-    **_,
-):
+          Source code in torrentfile\recheck.py
+          
374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
+403
+404
+405
def iter_pieces(self):
     """
-    Construct MetaFile superclass and assign local attributes.
+    Iterate through, and hash pieces of torrent contents.
+
+    Yields
+    ------
+    piece : bytes
+        hash digest for block of torrent data.
     """
-    self.private = private
-    self.cwd = cwd
-    self.outfile = outfile
-    self.progress = int(progress)
-    self.comment = comment
-    self.source = source
-    self.meta_version = meta_version
+    partial = bytearray()
+    for i, path in enumerate(self.paths):
+        total = self.fileinfo[i]["length"]
+        self.prog_start(total, path, unit="bytes")
+        self.index = i
+        if os.path.exists(path):
+            for piece in self.extract(path, partial):
+                self.prog_update(len(piece))
+                if (len(piece) == self.piece_length) or (
+                    i + 1 == len(self.paths)
+                ):
+                    yield piece
+                else:
+                    partial = piece
 
-    if content:
-        path = content
-    if not path:
-        if announce and len(announce) > 1 and os.path.exists(announce[-1]):
-            path = announce[-1]
-            announce = announce[:-1]
-        elif url_list and os.path.exists(url_list[-1]):
-            path = url_list[-1]
-            url_list = url_list[:-1]
-        elif httpseeds and os.path.exists(httpseeds[-1]):
-            path = httpseeds[-1]
-            httpseeds = httpseeds[:-1]
         else:
-            raise utils.MissingPathError("Path to content is required.")
-
-    # base path to torrent content.
-    self.path = path
+            length = self.fileinfo[i]["length"]
+            for pad in self._gen_padding(partial, length):
+                if len(pad) == self.piece_length:
+                    yield pad
+                else:
+                    partial = pad
+        self.prog_close()
+
+
+
+
- logger.debug("path parameter found %s", path) +
- # Format piece_length attribute. - if piece_length: - self.piece_length = utils.normalize_piece_length(piece_length) - logger.debug("piece length parameter found %s", piece_length) - else: - self.piece_length = utils.path_piece_length(self.path) - logger.debug("piece length calculated %s", self.piece_length) - # Assign announce URL to empty string if none provided. - if not announce: - self.announce, self.announce_list = "", [[""]] - # Most torrent clients have editting trackers as a feature. - elif isinstance(announce, str): - self.announce, self.announce_list = announce, [[announce]] - elif isinstance(announce, Sequence): - self.announce, self.announce_list = announce[0], [announce] - self.meta = { - "announce": self.announce, - "announce-list": self.announce_list, - "created by": f"TorrentFile:v{version}", - "creation date": int(datetime.timestamp(datetime.now())), - "info": {}, - } - if comment: - self.meta["info"]["comment"] = comment - logger.debug("comment parameter found %s", comment) - if private: - self.meta["info"]["private"] = 1 - logger.debug("private parameter triggered") - if source: - self.meta["info"]["source"] = source - logger.debug("source parameter found %s", source) - if url_list: - self.meta["url-list"] = url_list - logger.debug("url list parameter found %s", str(url_list)) - if httpseeds: - self.meta["httpseeds"] = httpseeds - logger.debug("httpseeds parameter found %s", str(httpseeds)) - self.meta["info"]["piece length"] = self.piece_length +
- self.meta_version = meta_version - parent, self.name = os.path.split(self.path) - if not self.name: - self.name = os.path.basename(parent) - self.meta["info"]["name"] = self.name -
-
-
+
-

-assemble(self) +

+ HashChecker -

+ + +
+

+ Bases: ProgMixin

-

Overload in subclasses.

-

Exceptions:

- - - - - - - - +

Verify that root hashes of content files match the .torrent files.

+ +

Parameters:

+
TypeDescription
+ - - + + + + - -
Exception

NotImplementedError

NameTypeDescriptionDefault
-
- Source code in torrentfile\torrent.py -
def assemble(self):
+    
+    
+        
+          checker
+          
+                Object
+          
+          

the checker instance that maintains variables.

+ + required + + + + hasher + + Object + +

the version specific hashing class for torrent content.

+ + required + + + + + + +
+ Source code in torrentfile\recheck.py +
479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
+641
+642
+643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
class HashChecker(ProgMixin):
     """
-    Overload in subclasses.
+    Verify that root hashes of content files match the .torrent files.
 
-    Raises
-    ------
-    Exception
-        NotImplementedError
+    Parameters
+    ----------
+    checker : Object
+        the checker instance that maintains variables.
+    hasher : Object
+        the version specific hashing class for torrent content.
     """
-    raise NotImplementedError
-
- - - + def __init__(self, checker: Checker): + """ + Construct a HybridChecker instance. + """ + self.checker = checker + self.paths = checker.paths + self.piece_length = checker.piece_length + self.fileinfo = checker.fileinfo + self.piece_layers = checker.meta["piece layers"] + self.current = None + self.index = -1 + + def __iter__(self): + """ + Assign iterator and return self. + """ + return self + + def __next__(self): + """ + Provide the result of comparison. + """ + if self.current is None: + self.next_file() + try: + return self.process_current() + except StopIteration as itererr: + if self.next_file(): + return self.process_current() + raise StopIteration from itererr + + class Padder: + """ + Padding class to generate padding hashes wherever needed. + + Parameters + ---------- + length: int + the total size of the mock file generating padding for. + piece_length : int + the block size that each hash represents. + """ + + def __init__(self, length, piece_length): + """ + Construct padding class to Mock missing or incomplete files. + Parameters + ---------- + length : int + size of the file + piece_length : int + the piece length for each iteration. + """ + self.length = length + self.piece_length = piece_length + self.pad = sha256(bytearray(piece_length)).digest() + def __iter__(self): + """ + Return self to correctly implement iterator type. + """ + return self # pragma: nocover -
+ def __next__(self) -> bytes: + """ + Iterate through seemingly endless sha256 hashes of zeros. + Returns + ------- + tuple : + returns the padding + Raises + ------ + StopIteration + """ + if self.length >= self.piece_length: + self.length -= self.piece_length + return self.pad + if self.length > 0: + pad = sha256(bytearray(self.length)).digest() + self.length -= self.length + return pad + raise StopIteration -

-set_callback(func) + def next_file(self) -> bool: + """ + Remove all references to processed files and prepare for the next. - - classmethod - + Returns + ------- + bool + if there is a next file found + """ + self.index += 1 + self.prog_close() + if self.current is None or self.index < len(self.paths): + self.current = self.paths[self.index] + self.length = self.fileinfo[self.index]["length"] + self.root_hash = self.fileinfo[self.index]["pieces root"] + if self.length > self.piece_length: + self.pieces = self.piece_layers[self.root_hash] + else: + self.pieces = self.root_hash + path = self.paths[self.index] + self.prog_start(self.length, path, unit="bytes") + self.count = 0 + if os.path.exists(self.current): + self.hasher = FileHasher(path, self.piece_length, progress=0) + else: + self.hasher = self.Padder(self.length, self.piece_length) + return True + if self.index >= len(self.paths): + del self.current + del self.length + del self.root_hash + del self.pieces + return False -

+ def process_current(self) -> tuple: + """ + Gather necessary information to compare to metafile details. -
+ Returns + ------- + tuple + a tuple containing the layer, piece, current path and size -

Assign a callback function for the Hashing class to call for each hash.

+ Raises + ------ + StopIteration + """ + try: + layer = next(self.hasher) + piece, size = self.advance() + self.prog_update(size) + return layer, piece, self.current, size + except StopIteration as err: + if self.length > 0 and self.count * SHA256 < len(self.pieces): + self.hasher = self.Padder(self.length, self.piece_length) + piece, size = self.advance() + layer = next(self.hasher) + self.prog_update(0) + return layer, piece, self.current, size + raise StopIteration from err -

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
funcfunction

The callback function which accepts a single paramter.

required
-
- Source code in torrentfile\torrent.py -
@classmethod
-def set_callback(cls, func):
-    """
-    Assign a callback function for the Hashing class to call for each hash.
+    def advance(self) -> tuple:
+        """
+        Increment the number of pieces processed for the current file.
 
-    Parameters
-    ----------
-    func : function
-        The callback function which accepts a single paramter.
-    """
-    if "hasher" in vars(cls) and vars(cls)["hasher"]:
-        cls.hasher.set_callback(func)
+        Returns
+        -------
+        tuple
+            the piece and size
+        """
+        start = self.count * SHA256
+        end = start + SHA256
+        piece = self.pieces[start:end]
+        self.count += 1
+        if self.length >= self.piece_length:
+            self.length -= self.piece_length
+            size = self.piece_length
+        else:
+            size = self.length
+            self.length -= self.length
+        return piece, size
 
-
-
+
+
-
+
+ -
-

-sort_meta(self) -

-
-

Sort the info and meta dictionaries.

-
- Source code in torrentfile\torrent.py -
def sort_meta(self):
-    """Sort the info and meta dictionaries."""
-    logger.debug("sorting dictionary keys")
-    meta = self.meta
-    meta["info"] = dict(sorted(list(meta["info"].items())))
-    meta = dict(sorted(list(meta.items())))
-    return meta
-
-
-
-
-
+
-

-write(self, outfile=None) +

+ Padder +

+
-

Write meta information to .torrent file.

-

Parameters:

- - - - - - - - - - +

Padding class to generate padding hashes wherever needed.

+ +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
outfilestr

Destination path for .torrent file. default=None

NoneNameTypeDescriptionDefault
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
tuple

Where the .torrent file was writen.

-
- Source code in torrentfile\torrent.py -
def write(self, outfile=None) -> tuple:
+    
+    
+        
+          length
+          
+          
+          

the total size of the mock file generating padding for.

+ + required + + + + piece_length + + int + +

the block size that each hash represents.

+ + required + + + + + + +
+ Source code in torrentfile\recheck.py +
522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
+548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
class Padder:
     """
-    Write meta information to .torrent file.
+    Padding class to generate padding hashes wherever needed.
 
     Parameters
     ----------
-    outfile : str
-        Destination path for .torrent file. default=None
-
-    Returns
-    -------
-    outfile : str
-        Where the .torrent file was writen.
-    meta : dict
-        .torrent meta information.
+    length: int
+        the total size of the mock file generating padding for.
+    piece_length : int
+        the block size that each hash represents.
     """
-    fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
-    if not self.outfile and not outfile:
-        if self.cwd:
-            self.outfile = fallback
-        else:
-            path = str(self.path).rstrip("\\/")
-            self.outfile = path + ".torrent"
-    elif outfile:
-        self.outfile = outfile
-    if str(self.outfile)[-1] in "\\/":
-        self.outfile = self.outfile + (self.name + ".torrent")
-    self.meta = self.sort_meta()
-    try:
-        pyben.dump(self.meta, self.outfile)
-    except PermissionError:
-        self.outfile = fallback
-        pyben.dump(self.meta, fallback)
-    return self.outfile, self.meta
-
- - - - - - - - - + def __init__(self, length, piece_length): + """ + Construct padding class to Mock missing or incomplete files. - + Parameters + ---------- + length : int + size of the file + piece_length : int + the piece length for each iteration. + """ + self.length = length + self.piece_length = piece_length + self.pad = sha256(bytearray(piece_length)).digest() - + def __iter__(self): + """ + Return self to correctly implement iterator type. + """ + return self # pragma: nocover + def __next__(self) -> bytes: + """ + Iterate through seemingly endless sha256 hashes of zeros. + Returns + ------- + tuple : + returns the padding -
+ Raises + ------ + StopIteration + """ + if self.length >= self.piece_length: + self.length -= self.piece_length + return self.pad + if self.length > 0: + pad = sha256(bytearray(self.length)).digest() + self.length -= self.length + return pad + raise StopIteration +
+
+
-

- -TorrentAssembler (MetaFile) - +
-

-
-

Assembler class for Bittorrent version 2 and hybrid meta files.

-

This differs from the TorrentFileV2 and TorrentFileHybrid, because -it can be used as an iterator and works for both versions.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
kwargsdict

Keyword arguments for torrent options.

{}
-
- Source code in torrentfile\torrent.py -
class TorrentAssembler(MetaFile):
-    """
-    Assembler class for Bittorrent version 2 and hybrid meta files.
 
-    This differs from the TorrentFileV2 and TorrentFileHybrid, because
-    it can be used as an iterator and works for both versions.
 
-    Parameters
-    ----------
-    kwargs : dict
-        Keyword arguments for torrent options.
-    """
 
-    hasher = FileHasher
 
-    def __init__(self, **kwargs):
-        """
-        Create Bittorrent v1 v2 hybrid metafiles.
-        """
-        super().__init__(**kwargs)
-        logger.debug("Assembling bittorrent Hybrid file")
-        self.name = os.path.basename(self.path)
-        self.hashes = []
-        self.piece_layers = {}
-        self.pieces = bytearray()
-        self.files = []
-        self.hybrid = self.meta_version == "3"
-        self.total = len(utils.get_file_list(self.path))
-        self.assemble()
 
-    def assemble(self):
-        """
-        Assemble the parts of the torrentfile into meta dictionary.
-        """
-        info = self.meta["info"]
-        info["meta version"] = 2
+  
- if os.path.isfile(self.path): - info["file tree"] = {self.name: self._traverse(self.path)} - info["length"] = os.path.getsize(self.path) - else: - info["file tree"] = self._traverse(self.path) - if self.hybrid: - info["files"] = self.files - if self.hybrid: - info["pieces"] = self.pieces - self.meta["piece layers"] = self.piece_layers - return info +
+__init__(length, piece_length) - def _traverse(self, path: str) -> dict: - """ - Build meta dictionary while walking directory. - Parameters - ---------- - path : str - Path to target file. - """ - if os.path.isfile(path): - file_size = os.path.getsize(path) - if self.hybrid: - self.files.append( - { - "length": file_size, - "path": os.path.relpath(path, self.path).split(os.sep), - } - ) +
- if file_size == 0: - return {"": {"length": file_size}} - logger.debug("Hashing %s", str(path)) - hasher = FileHasher( - path, self.piece_length, progress=True, hybrid=self.hybrid - ) - layers = bytearray() - for result in hasher: - if self.hybrid: - layer_hash, piece = result - self.pieces.extend(piece) - else: - layer_hash = result - layers.extend(layer_hash) - if file_size > self.piece_length: - self.piece_layers[hasher.root] = layers - if self.hybrid and hasher.padding_file: - self.files.append(hasher.padding_file) +
- return {"": {"length": file_size, "pieces root": hasher.root}} +

Construct padding class to Mock missing or incomplete files.

- tree = {} - if os.path.isdir(path): - for name in sorted(os.listdir(path)): - tree[name] = self._traverse(os.path.join(path, name)) - return tree +

Parameters:

+ + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
length + int +

size of the file

+ required +
piece_length + int +

the piece length for each iteration.

+ required +
+ +
+ Source code in torrentfile\recheck.py +
534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
+546
+547
def __init__(self, length, piece_length):
+    """
+    Construct padding class to Mock missing or incomplete files.
+
+    Parameters
+    ----------
+    length : int
+        size of the file
+    piece_length : int
+        the piece length for each iteration.
+    """
+    self.length = length
+    self.piece_length = piece_length
+    self.pad = sha256(bytearray(piece_length)).digest()
 
+
+
+
-
+ +
+
+__iter__() +
-
+
+ +

Return self to correctly implement iterator type.

+ +
+ Source code in torrentfile\recheck.py +
549
+550
+551
+552
+553
def __iter__(self):
+    """
+    Return self to correctly implement iterator type.
+    """
+    return self  # pragma: nocover
+
+
+
+
+ +
-

- -hasher (CbMixin, ProgMixin) - +
-

+
+__next__() + + +
+
-

Calculate root and piece hashes for creating hybrid torrent file.

-

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. -With a branching factor of 2, merge layer hashes until blocks equal -piece_length bytes for the piece layer, and then the root hash.

+

Iterate through seemingly endless sha256 hashes of zeros.

-

Parameters:

- - - - - - - - - - - - - - - - +

Returns:

+
NameTypeDescriptionDefault
pathstr

path to target file.

required
+ - - - - + + + + + + + + + +
piece_lengthint

piece length for data chunks.

requiredName TypeDescription
tuple + bytes +

returns the padding

+ +

Raises:

+ + - - - - + + - -
progressbool

default = None

TrueTypeDescription
+ + + + + StopIteration + + + + + +
- Source code in torrentfile\torrent.py -
class FileHasher(CbMixin, ProgMixin):
+          Source code in torrentfile\recheck.py
+          
555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
def __next__(self) -> bytes:
     """
-    Calculate root and piece hashes for creating hybrid torrent file.
+    Iterate through seemingly endless sha256 hashes of zeros.
 
-    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
-    With a branching factor of 2, merge layer hashes until blocks equal
-    piece_length bytes for the piece layer, and then the root hash.
+    Returns
+    -------
+    tuple :
+        returns the padding
 
-    Parameters
-    ----------
-    path : str
-        path to target file.
-    piece_length : int
-        piece length for data chunks.
-    progress : int
-        default = None
+    Raises
+    ------
+    StopIteration
     """
+    if self.length >= self.piece_length:
+        self.length -= self.piece_length
+        return self.pad
+    if self.length > 0:
+        pad = sha256(bytearray(self.length)).digest()
+        self.length -= self.length
+        return pad
+    raise StopIteration
+
+
+
+
- def __init__( - self, - path: str, - piece_length: int, - progress: bool = True, - hybrid: bool = False, - ): - """ - Construct Hasher class instances for each file in torrent. - """ - self.path = path - self.piece_length = piece_length - self.pieces = [] - self.layer_hashes = [] - self.piece_layer = None - self.root = None - self.padding_piece = None - self.padding_file = None - self.amount = piece_length // BLOCK_SIZE - self.end = False - self.current = open(path, "rb") - self.hybrid = hybrid - if progress: - self.progressbar = True - self.prog_start(os.path.getsize(path), path, unit="bytes") +
- def __iter__(self): - """Return `self`: needed to implement iterator implementation.""" - return self - def _pad_remaining(self, block_count: int): - """ - Generate Hash sized, 0 filled bytes for padding. - Parameters - ---------- - block_count : int - current total number of blocks collected. - Returns - ------- - padding : bytes - Padding to fill remaining portion of tree. - """ - # when the there is only one block for file - remaining = self.amount - block_count - if not self.layer_hashes: - power2 = next_power_2(block_count) - remaining = power2 - block_count - self.prog_update(HASH_SIZE * remaining) - return [bytes(HASH_SIZE) for _ in range(remaining)] - def __next__(self): - """ - Calculate layer hashes for contents of file. +
- Parameters - ---------- - data : BytesIO - File opened in read mode. - """ - if self.end: - self.end = False - raise StopIteration - plength = self.piece_length - blocks = [] - piece = sha1() # nosec - total = 0 - block = bytearray(BLOCK_SIZE) - for _ in range(self.amount): - size = self.current.readinto(block) - self.prog_update(size) - if not size: - self.end = True - break - total += size - plength -= size - blocks.append(sha256(block[:size]).digest()) - if self.hybrid: - piece.update(block[:size]) - if len(blocks) != self.amount: - padding = self._pad_remaining(len(blocks)) - blocks.extend(padding) - layer_hash = merkle_root(blocks) - self.layer_hashes.append(layer_hash) - if self._cb: - self._cb(layer_hash) - if self.end: - self._calculate_root() - self.prog_close() - if self.hybrid: - if plength > 0: - self.padding_file = { - "attr": "p", - "length": plength, - "path": [".pad", str(plength)], - } - piece.update(bytes(plength)) - piece = piece.digest() - self.pieces.append(piece) - return layer_hash, piece - return layer_hash +
+ +
+ + + + +
+ + + +

+__init__(checker) - def _calculate_root(self): - """ - Calculate the root hash for opened file. - """ - self.piece_layer = b"".join(self.layer_hashes) - if len(self.layer_hashes) > 1: - pad_piece = merkle_root([bytes(32) for _ in range(self.amount)]) +

- pow2 = next_power_2(len(self.layer_hashes)) - remainder = pow2 - len(self.layer_hashes) - self.layer_hashes += [pad_piece for _ in range(remainder)] - self.root = merkle_root(self.layer_hashes) - self.current.close() +
+ +

Construct a HybridChecker instance.

+ +
+ Source code in torrentfile\recheck.py +
491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
def __init__(self, checker: Checker):
+    """
+    Construct a HybridChecker instance.
+    """
+    self.checker = checker
+    self.paths = checker.paths
+    self.piece_length = checker.piece_length
+    self.fileinfo = checker.fileinfo
+    self.piece_layers = checker.meta["piece layers"]
+    self.current = None
+    self.index = -1
 
+
+
+ +
-
+
+ + + +

+__iter__() + + +

+
+

Assign iterator and return self.

+
+ Source code in torrentfile\recheck.py +
503
+504
+505
+506
+507
def __iter__(self):
+    """
+    Assign iterator and return self.
+    """
+    return self
+
+
+
+
+
+
-
+

+__next__() -

-__init__(self, path, piece_length, progress=True, hybrid=False) - - special - +
-
-

Construct Hasher class instances for each file in torrent.

+

Provide the result of comparison.

- Source code in torrentfile\torrent.py -
def __init__(
-    self,
-    path: str,
-    piece_length: int,
-    progress: bool = True,
-    hybrid: bool = False,
-):
+          Source code in torrentfile\recheck.py
+          
509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
def __next__(self):
     """
-    Construct Hasher class instances for each file in torrent.
+    Provide the result of comparison.
     """
-    self.path = path
-    self.piece_length = piece_length
-    self.pieces = []
-    self.layer_hashes = []
-    self.piece_layer = None
-    self.root = None
-    self.padding_piece = None
-    self.padding_file = None
-    self.amount = piece_length // BLOCK_SIZE
-    self.end = False
-    self.current = open(path, "rb")
-    self.hybrid = hybrid
-    if progress:
-        self.progressbar = True
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
+    if self.current is None:
+        self.next_file()
+    try:
+        return self.process_current()
+    except StopIteration as itererr:
+        if self.next_file():
+            return self.process_current()
+        raise StopIteration from itererr
 
+
@@ -20939,29 +30550,82 @@
+
+ +

+advance() -

-__iter__(self) - - special - +
-
-

Return self: needed to implement iterator implementation.

+

Increment the number of pieces processed for the current file.

+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple +

the piece and size

- Source code in torrentfile\torrent.py -
def __iter__(self):
-    """Return `self`: needed to implement iterator implementation."""
-    return self
+          Source code in torrentfile\recheck.py
+          
638
+639
+640
+641
+642
+643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
def advance(self) -> tuple:
+    """
+    Increment the number of pieces processed for the current file.
+
+    Returns
+    -------
+    tuple
+        the piece and size
+    """
+    start = self.count * SHA256
+    end = start + SHA256
+    piece = self.pieces[start:end]
+    self.count += 1
+    if self.length >= self.piece_length:
+        self.length -= self.piece_length
+        size = self.piece_length
+    else:
+        size = self.length
+        self.length -= self.length
+    return piece, size
 
+
@@ -20969,95 +30633,221 @@
+
-
-__next__(self) +

+next_file() - - special - -

+
+
-

Calculate layer hashes for contents of file.

+

Remove all references to processed files and prepare for the next.

-

Parameters:

- - - - - - - - - - +

Returns:

+
NameTypeDescriptionDefault
+ - - - - + + - -
dataBytesIO

File opened in read mode.

requiredTypeDescription
+ + + + + bool + +

if there is a next file found

+ + + +
- Source code in torrentfile\torrent.py -
def __next__(self):
+          Source code in torrentfile\recheck.py
+          
577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
def next_file(self) -> bool:
     """
-    Calculate layer hashes for contents of file.
+    Remove all references to  processed files and prepare for the next.
 
-    Parameters
-    ----------
-    data : BytesIO
-        File opened in read mode.
+    Returns
+    -------
+    bool
+        if there is a next file found
     """
-    if self.end:
-        self.end = False
-        raise StopIteration
-    plength = self.piece_length
-    blocks = []
-    piece = sha1()  # nosec
-    total = 0
-    block = bytearray(BLOCK_SIZE)
-    for _ in range(self.amount):
-        size = self.current.readinto(block)
+    self.index += 1
+    self.prog_close()
+    if self.current is None or self.index < len(self.paths):
+        self.current = self.paths[self.index]
+        self.length = self.fileinfo[self.index]["length"]
+        self.root_hash = self.fileinfo[self.index]["pieces root"]
+        if self.length > self.piece_length:
+            self.pieces = self.piece_layers[self.root_hash]
+        else:
+            self.pieces = self.root_hash
+        path = self.paths[self.index]
+        self.prog_start(self.length, path, unit="bytes")
+        self.count = 0
+        if os.path.exists(self.current):
+            self.hasher = FileHasher(path, self.piece_length, progress=0)
+        else:
+            self.hasher = self.Padder(self.length, self.piece_length)
+        return True
+    if self.index >= len(self.paths):
+        del self.current
+        del self.length
+        del self.root_hash
+        del self.pieces
+    return False
+
+
+
+
+ +
+ + + +
+ + + +

+process_current() + + +

+ + +
+ +

Gather necessary information to compare to metafile details.

+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ tuple +

a tuple containing the layer, piece, current path and size

+ +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ StopIteration +
+ +
+ Source code in torrentfile\recheck.py +
611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
def process_current(self) -> tuple:
+    """
+    Gather necessary information to compare to metafile details.
+
+    Returns
+    -------
+    tuple
+        a tuple containing the layer, piece, current path and size
+
+    Raises
+    ------
+    StopIteration
+    """
+    try:
+        layer = next(self.hasher)
+        piece, size = self.advance()
         self.prog_update(size)
-        if not size:
-            self.end = True
-            break
-        total += size
-        plength -= size
-        blocks.append(sha256(block[:size]).digest())
-        if self.hybrid:
-            piece.update(block[:size])
-    if len(blocks) != self.amount:
-        padding = self._pad_remaining(len(blocks))
-        blocks.extend(padding)
-    layer_hash = merkle_root(blocks)
-    self.layer_hashes.append(layer_hash)
-    if self._cb:
-        self._cb(layer_hash)
-    if self.end:
-        self._calculate_root()
-        self.prog_close()
-    if self.hybrid:
-        if plength > 0:
-            self.padding_file = {
-                "attr": "p",
-                "length": plength,
-                "path": [".pad", str(plength)],
-            }
-            piece.update(bytes(plength))
-        piece = piece.digest()
-        self.pieces.append(piece)
-        return layer_hash, piece
-    return layer_hash
+        return layer, piece, self.current, size
+    except StopIteration as err:
+        if self.length > 0 and self.count * SHA256 < len(self.pieces):
+            self.hasher = self.Padder(self.length, self.piece_length)
+            piece, size = self.advance()
+            layer = next(self.hasher)
+            self.prog_update(0)
+            return layer, piece, self.current, size
+        raise StopIteration from err
 
+
@@ -21077,98 +30867,234 @@
- - - -

-__init__(self, **kwargs) - - - special - - -

- -
-

Create Bittorrent v1 v2 hybrid metafiles.

+
-
- Source code in torrentfile\torrent.py -
def __init__(self, **kwargs):
-    """
-    Create Bittorrent v1 v2 hybrid metafiles.
-    """
-    super().__init__(**kwargs)
-    logger.debug("Assembling bittorrent Hybrid file")
-    self.name = os.path.basename(self.path)
-    self.hashes = []
-    self.piece_layers = {}
-    self.pieces = bytearray()
-    self.files = []
-    self.hybrid = self.meta_version == "3"
-    self.total = len(utils.get_file_list(self.path))
-    self.assemble()
-
-
-
- - - -

-assemble(self) +
-

-
+

+ torrentfile.torrent -

Assemble the parts of the torrentfile into meta dictionary.

-
- Source code in torrentfile\torrent.py -
def assemble(self):
-    """
-    Assemble the parts of the torrentfile into meta dictionary.
-    """
-    info = self.meta["info"]
-    info["meta version"] = 2
 
-    if os.path.isfile(self.path):
-        info["file tree"] = {self.name: self._traverse(self.path)}
-        info["length"] = os.path.getsize(self.path)
+

- else: - info["file tree"] = self._traverse(self.path) - if self.hybrid: - info["files"] = self.files +
- if self.hybrid: - info["pieces"] = self.pieces - self.meta["piece layers"] = self.piece_layers - return info -
-
-
+

Classes and procedures pertaining to the creation of torrent meta files.

+

Classes

+
    +
  • +

    TorrentFile + construct .torrent file.

    +
  • +
  • +

    TorrentFileV2 + construct .torrent v2 files using provided data.

    +
  • +
  • +

    MetaFile + base class for all MetaFile classes.

    +
  • +
+

Constants

+
    +
  • +

    BLOCK_SIZE : int + size of leaf hashes for merkle tree.

    +
  • +
  • +

    HASH_SIZE : int + Length of a sha256 hash.

    +
  • +
+

Bittorrent V2

+

From Bittorrent.org Documentation pages.

+

Implementation details for Bittorrent Protocol v2.

+
+

Note

+

All strings in a .torrent file that contain text must be UTF-8 encoded.

+
+
Meta Version 2 Dictionary:
+
    +
  • +

    "announce": + The URL of the tracker.

    +
  • +
  • +

    "info": + This maps to a dictionary, with keys described below.

    +
      +
    • +

      "name": + A display name for the torrent. It is purely advisory.

      +
    • +
    • +

      "piece length": + The number of bytes that each logical piece in the peer + protocol refers to. I.e. it sets the granularity of piece, request, + bitfield and have messages. It must be a power of two and at least + 6KiB.

      +
    • +
    • +

      "meta version": + An integer value, set to 2 to indicate compatibility + with the current revision of this specification. Version 1 is not + assigned to avoid confusion with BEP3. Future revisions will only + increment this issue to indicate an incompatible change has been made, + for example that hash algorithms were changed due to newly discovered + vulnerabilities. Lementations must check this field first and indicate + that a torrent is of a newer version than they can handle before + performing other idations which may result in more general messages + about invalid files. Files are mapped into this piece address space so + that each non-empty

      +
    • +
    • +

      "file tree": + A tree of dictionaries where dictionary keys represent UTF-8 + encoded path elements. Entries with zero-length keys describe the + properties of the composed path at that point. 'UTF-8 encoded' + context only means that if the native encoding is known at creation + time it must be converted to UTF-8. Keys may contain invalid UTF-8 + sequences or characters and names that are reserved on specific + filesystems. Implementations must be prepared to sanitize them. On + platforms path components exactly matching '.' and '..' must be + sanitized since they could lead to directory traversal attacks and + conflicting path descriptions. On platforms that require UTF-8 + path components this sanitizing step must happen after normalizing + overlong UTF-8 encodings. + File is aligned to a piece boundary and occurs in same order as + the file tree. The last piece of each file may be shorter than the + specified piece length, resulting in an alignment gap.

      +
    • +
    • +

      "length": + Length of the file in bytes. Presence of this field indicates + that the dictionary describes a file, not a directory. Which means + it must not have any sibling entries.

      +
    • +
    • +

      "pieces root": + For non-empty files this is the the root hash of a merkle + tree with a branching factor of 2, constructed from 16KiB blocks + of the file. The last block may be shorter than 16KiB. The + remaining leaf hashes beyond the end of the file required to + construct upper layers of the merkle tree are set to zero. As of + meta version 2 SHA2-256 is used as digest function for the merkle + tree. The hash is stored in its binary form, not as human-readable + string.

      +
    • +
    +
  • +
  • +

    "piece layers": + A dictionary of strings. For each file in the file tree that + is larger than the piece size it contains one string value. + The keys are the merkle roots while the values consist of concatenated + hashes of one layer within that merkle tree. The layer is chosen so + that one hash covers piece length bytes. For example if the piece + size is 16KiB then the leaf hashes are used. If a piece size of + 128KiB is used then 3rd layer up from the leaf hashes is used. Layer + hashes which exclusively cover data beyond the end of file, i.e. + are only needed to balance the tree, are omitted. All hashes are + stored in their binary format. A torrent is not valid if this field is + absent, the contained hashes do not match the merkle roots or are + not from the correct layer.

    +
  • +
+
+

Important

+

The file tree root dictionary itself must not be a file, +i.e. it must not contain a zero-length key with a dictionary containing +a length key.

+
+

Bittorrent V1

+
v1 meta-dictionary
+
    +
  • +

    announce: + The URL of the tracker.

    +
  • +
  • +

    info: + This maps to a dictionary, with keys described below.

    +
      +
    • +

      name: + maps to a UTF-8 encoded string which is the suggested name to + save the file (or directory) as. It is purely advisory.

      +
    • +
    • +

      piece length: + maps to the number of bytes in each piece the file is split + into. For the purposes of transfer, files are split into + fixed-size pieces which are all the same length except for + possibly the last one which may be truncated.

      +
    • +
    • +

      piece length: + is almost always a power of two, most commonly 2^18 = 256 K

      +
    • +
    • +

      pieces: + maps to a string whose length is a multiple of 20. It is to be + subdivided into strings of length 20, each of which is the SHA1 + hash of the piece at the corresponding index.

      +
    • +
    • +

      length: + In the single file case, maps to the length of the file in bytes.

      +
    • +
    • +

      files: + If present then the download represents a single file, otherwise it + represents a set of files which go in a directory structure. + For the purposes of the other keys, the multi-file case is treated + as only having a single file by concatenating the files in the order + they appear in the files list. The files list is the value files + maps to, and is a list of dictionaries containing the following keys:

      +
        +
      • +

        path: + A list of UTF-8 encoded strings corresponding to subdirectory + names, the last of which is the actual file name

        +
      • +
      • +

        length: + Maps to the length of the file in bytes.

        +
      • +
      +
    • +
    • +

      length: + Only present if the content is a single file. Maps to the length + of the file in bytes.

      +
    • +
    +
  • +
+
+

Note

+

In the single file case, the name key is the name of a file, +in the muliple file case, it's the name of a directory.

+
-
+
-
-
-
@@ -21176,98 +31102,566 @@

-

- -TorrentFile (MetaFile, ProgMixin) - +

+ MetaFile

+
-

Class for creating Bittorrent meta files.

-

Construct Torrentfile class instance object.

-

Parameters:

- - - - - - - - - - +

Base Class for all TorrentFile classes.

+ +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
kwargsdict

Dictionary containing torrent file options.

{}NameTypeDescriptionDefault
-
- Source code in torrentfile\torrent.py -
class TorrentFile(MetaFile, ProgMixin):
+    
+    
+        
+          path
+          
+                str
+          
+          

target path to torrent content. Default: None

+ + None + + + + announce + + str + +

One or more tracker URL's. Default: None

+ + None + + + + comment + + str + +

A comment. Default: None

+ + None + + + + piece_length + + int + +

Size of torrent pieces. Default: None

+ + None + + + + private + + bool + +

For private trackers. Default: None

+ + False + + + + outfile + + str + +

target path to write .torrent file. Default: None

+ + None + + + + source + + str + +

Private tracker source. Default: None

+ + None + + + + progress + + str + +

level of progress bar displayed Default: "1"

+ + 1 + + + + cwd + + bool + +

If True change default save location to current directory

+ + False + + + + httpseeds + + list + +

one or more web addresses where torrent content can be found.

+ + None + + + + url_list + + list + +

one or more web addressess where torrent content exists.

+ + None + + + + content + + str + +

alias for 'path' arg.

+ + None + + + + meta_version + + int + +

indicates which Bittorrent protocol to use for hashing content

+ + None + + + + + + +
+ Source code in torrentfile\torrent.py +
204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
+354
+355
+356
+357
+358
+359
+360
+361
+362
+363
+364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
class MetaFile:
     """
-    Class for creating Bittorrent meta files.
-
-    Construct *Torrentfile* class instance object.
+    Base Class for all TorrentFile classes.
 
     Parameters
     ----------
-    kwargs : dict
-        Dictionary containing torrent file options.
+    path : str
+        target path to torrent content.  Default: None
+    announce : str
+        One or more tracker URL's.  Default: None
+    comment : str
+        A comment.  Default: None
+    piece_length : int
+        Size of torrent pieces.  Default: None
+    private : bool
+        For private trackers.  Default: None
+    outfile : str
+        target path to write .torrent file. Default: None
+    source : str
+        Private tracker source. Default: None
+    progress : str
+        level of progress bar displayed  Default: "1"
+    cwd : bool
+        If True change default save location to current directory
+    httpseeds : list
+        one or more web addresses where torrent content can be found.
+    url_list : list
+        one or more web addressess where torrent content exists.
+    content : str
+        alias for 'path' arg.
+    meta_version : int
+        indicates which Bittorrent protocol to use for hashing content
     """
 
-    hasher = Hasher
+    hasher = None
 
-    def __init__(self, **kwargs):
+    @classmethod
+    def set_callback(cls, func):
         """
-        Construct TorrentFile instance with given keyword args.
+        Assign a callback function for the Hashing class to call for each hash.
 
         Parameters
         ----------
-        kwargs : dict
-            dictionary of keyword args passed to superclass.
+        func : function
+            The callback function which accepts a single paramter.
         """
-        super().__init__(**kwargs)
-        logger.debug("Assembling bittorrent v1 torrent file")
-        self.assemble()
+        if "hasher" in vars(cls) and vars(cls)["hasher"]:
+            cls.hasher.set_callback(func)
+
+    def __init__(
+        self,
+        path=None,
+        announce=None,
+        comment=None,
+        piece_length=None,
+        private=False,
+        outfile=None,
+        source=None,
+        progress=1,
+        cwd=False,
+        httpseeds=None,
+        url_list=None,
+        content=None,
+        meta_version=None,
+        **_,
+    ):
+        """
+        Construct MetaFile superclass and assign local attributes.
+        """
+        self.private = private
+        self.cwd = cwd
+        self.outfile = outfile
+        self.progress = int(progress)
+        self.comment = comment
+        self.source = source
+        self.meta_version = meta_version
+
+        if content:
+            path = content
+        if not path:
+            if announce and len(announce) > 1 and os.path.exists(announce[-1]):
+                path = announce[-1]
+                announce = announce[:-1]
+            elif url_list and os.path.exists(url_list[-1]):
+                path = url_list[-1]
+                url_list = url_list[:-1]
+            elif httpseeds and os.path.exists(httpseeds[-1]):
+                path = httpseeds[-1]
+                httpseeds = httpseeds[:-1]
+            else:
+                raise utils.MissingPathError("Path to content is required.")
+
+        # base path to torrent content.
+        self.path = path
+
+        logger.debug("path parameter found %s", path)
+
+        # Format piece_length attribute.
+        if piece_length:
+            self.piece_length = utils.normalize_piece_length(piece_length)
+            logger.debug("piece length parameter found %s", piece_length)
+        else:
+            self.piece_length = utils.path_piece_length(self.path)
+            logger.debug("piece length calculated %s", self.piece_length)
+
+        # Assign announce URL to empty string if none provided.
+        if not announce:
+            self.announce, self.announce_list = "", [[""]]
+
+        # Most torrent clients have editting trackers as a feature.
+        elif isinstance(announce, str):
+            self.announce, self.announce_list = announce, [[announce]]
+
+        elif isinstance(announce, Sequence):
+            self.announce, self.announce_list = announce[0], [announce]
+
+        self.meta = {
+            "announce": self.announce,
+            "announce-list": self.announce_list,
+            "created by": f"TorrentFile:v{version}",
+            "creation date": int(datetime.timestamp(datetime.now())),
+            "info": {},
+        }
+        if comment:
+            self.meta["info"]["comment"] = comment
+            logger.debug("comment parameter found %s", comment)
+        if private:
+            self.meta["info"]["private"] = 1
+            logger.debug("private parameter triggered")
+        if source:
+            self.meta["info"]["source"] = source
+            logger.debug("source parameter found %s", source)
+        if url_list:
+            self.meta["url-list"] = url_list
+            logger.debug("url list parameter found %s", str(url_list))
+        if httpseeds:
+            self.meta["httpseeds"] = httpseeds
+            logger.debug("httpseeds parameter found %s", str(httpseeds))
+        self.meta["info"]["piece length"] = self.piece_length
+
+        self.meta_version = meta_version
+        parent, self.name = os.path.split(self.path)
+        if not self.name:
+            self.name = os.path.basename(parent)
+        self.meta["info"]["name"] = self.name
 
     def assemble(self):
         """
-        Assemble components of torrent metafile.
+        Overload in subclasses.
 
-        Returns
-        -------
-        dict
-            metadata dictionary for torrent file
+        Raises
+        ------
+        Exception
+            NotImplementedError
         """
-        info = self.meta["info"]
-        size, filelist = utils.filelist_total(self.path)
-        if os.path.isfile(self.path):
-            info["length"] = size
-        else:
-            info["files"] = [
-                {
-                    "length": os.path.getsize(path),
-                    "path": os.path.relpath(path, self.path).split(os.sep),
-                }
-                for path in filelist
-            ]
-        pieces = bytearray()
+        raise NotImplementedError
 
-        feeder = Hasher(filelist, self.piece_length, self.progress)
-        for piece in feeder:
-            pieces.extend(piece)
+    def sort_meta(self):
+        """Sort the info and meta dictionaries."""
+        logger.debug("sorting dictionary keys")
+        meta = self.meta
+        meta["info"] = dict(sorted(list(meta["info"].items())))
+        meta = dict(sorted(list(meta.items())))
+        return meta
 
-        info["pieces"] = pieces
+    def write(self, outfile=None) -> tuple:
+        """
+        Write meta information to .torrent file.
+
+        Parameters
+        ----------
+        outfile : str
+            Destination path for .torrent file. default=None
+
+        Returns
+        -------
+        outfile : str
+            Where the .torrent file was writen.
+        meta : dict
+            .torrent meta information.
+        """
+        fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
+        if not self.outfile and not outfile:
+            if self.cwd:
+                self.outfile = fallback
+            else:
+                path = str(self.path).rstrip("\\/")
+                self.outfile = path + ".torrent"
+        elif outfile:
+            self.outfile = outfile
+        if str(self.outfile)[-1] in "\\/":
+            self.outfile = self.outfile + (self.name + ".torrent")
+        self.meta = self.sort_meta()
+        try:
+            pyben.dump(self.meta, self.outfile)
+        except PermissionError:
+            self.outfile = fallback
+            pyben.dump(self.meta, fallback)
+        return self.outfile, self.meta
 
- +
+
@@ -21279,214 +31673,292 @@

-
-

- -hasher (CbMixin, ProgMixin) - -

-
-

Piece hasher for Bittorrent V1 files.

-

Takes a sorted list of all file paths, calculates sha1 hash -for fixed size pieces of file data from each file -seemlessly until the last piece which may be smaller than others.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
pathslist

List of files.

required
piece_lengthint

Size of chuncks to split the data into.

required
progressbool

default = None

True
-
- Source code in torrentfile\torrent.py -
class Hasher(CbMixin, ProgMixin):
-    """
-    Piece hasher for Bittorrent V1 files.
 
-    Takes a sorted list of all file paths, calculates sha1 hash
-    for fixed size pieces of file data from each file
-    seemlessly until the last piece which may be smaller than others.
 
-    Parameters
-    ----------
-    paths : list
-        List of files.
-    piece_length : int
-        Size of chuncks to split the data into.
-    progress : int
-        default = None
-    """
 
-    def __init__(self, paths: list, piece_length: int, progress: bool = True):
-        """Generate hashes of piece length data from filelist contents."""
-        self.piece_length = piece_length
-        self.paths = paths
-        self.progress = progress
-        self.total = sum([os.path.getsize(i) for i in self.paths])
-        self.index = 0
-        self.current = open(self.paths[0], "rb")
-        if self.progress:
-            total = os.path.getsize(self.paths[0])
-            self.prog_start(total, self.paths[0], unit="bytes")
-        logger.debug("Hashing %s", str(self.paths[0]))
 
-    def __iter__(self):
-        """
-        Iterate through feed pieces.
+  
- Returns - ------- - self : iterator - Iterator for leaves/hash pieces. - """ - return self - def _handle_partial(self, arr: bytearray) -> bytearray: - """ - Define the handling partial pieces that span 2 or more files. - Parameters - ---------- - arr : bytearray - Incomplete piece containing partial data +

+__init__(path=None, announce=None, comment=None, piece_length=None, private=False, outfile=None, source=None, progress=1, cwd=False, httpseeds=None, url_list=None, content=None, meta_version=None, **_) - Returns - ------- - digest : bytearray - SHA1 digest of the complete piece. - """ - while len(arr) < self.piece_length and self.next_file(): - target = self.piece_length - len(arr) - temp = bytearray(target) - size = self.current.readinto(temp) - arr.extend(temp[:size]) - self.prog_update(size) - if size == target: - break - return sha1(arr).digest() # nosec - def next_file(self) -> bool: - """ - Seemlessly transition to next file in file list. +

- Returns - ------- - bool: - True if there is a next file otherwise False. - """ - self.index += 1 - self.prog_close() - if self.index < len(self.paths): - path = self.paths[self.index] - logger.debug("Hashing %s", str(path)) - self.current.close() - if self.progress: - self.prog_start(os.path.getsize(path), path, unit="bytes") - self.current = open(path, "rb") - return True - return False - def __next__(self) -> bytes: - """ - Generate piece-length pieces of data from input file list. +
- Returns - ------- - bytes - SHA1 hash of the piece extracted. - """ - while True: - piece = bytearray(self.piece_length) - size = self.current.readinto(piece) - if size == 0: - if not self.next_file(): - raise StopIteration - elif size < self.piece_length: - self.prog_update(size) - return self._handle_partial(piece[:size]) - else: - self.prog_update(size) - return sha1(piece).digest() # nosec -
-
+

Construct MetaFile superclass and assign local attributes.

+ +
+ Source code in torrentfile\torrent.py +
253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
def __init__(
+    self,
+    path=None,
+    announce=None,
+    comment=None,
+    piece_length=None,
+    private=False,
+    outfile=None,
+    source=None,
+    progress=1,
+    cwd=False,
+    httpseeds=None,
+    url_list=None,
+    content=None,
+    meta_version=None,
+    **_,
+):
+    """
+    Construct MetaFile superclass and assign local attributes.
+    """
+    self.private = private
+    self.cwd = cwd
+    self.outfile = outfile
+    self.progress = int(progress)
+    self.comment = comment
+    self.source = source
+    self.meta_version = meta_version
+
+    if content:
+        path = content
+    if not path:
+        if announce and len(announce) > 1 and os.path.exists(announce[-1]):
+            path = announce[-1]
+            announce = announce[:-1]
+        elif url_list and os.path.exists(url_list[-1]):
+            path = url_list[-1]
+            url_list = url_list[:-1]
+        elif httpseeds and os.path.exists(httpseeds[-1]):
+            path = httpseeds[-1]
+            httpseeds = httpseeds[:-1]
+        else:
+            raise utils.MissingPathError("Path to content is required.")
 
+    # base path to torrent content.
+    self.path = path
 
+    logger.debug("path parameter found %s", path)
 
-  
+ # Format piece_length attribute. + if piece_length: + self.piece_length = utils.normalize_piece_length(piece_length) + logger.debug("piece length parameter found %s", piece_length) + else: + self.piece_length = utils.path_piece_length(self.path) + logger.debug("piece length calculated %s", self.piece_length) + + # Assign announce URL to empty string if none provided. + if not announce: + self.announce, self.announce_list = "", [[""]] + # Most torrent clients have editting trackers as a feature. + elif isinstance(announce, str): + self.announce, self.announce_list = announce, [[announce]] + elif isinstance(announce, Sequence): + self.announce, self.announce_list = announce[0], [announce] + self.meta = { + "announce": self.announce, + "announce-list": self.announce_list, + "created by": f"TorrentFile:v{version}", + "creation date": int(datetime.timestamp(datetime.now())), + "info": {}, + } + if comment: + self.meta["info"]["comment"] = comment + logger.debug("comment parameter found %s", comment) + if private: + self.meta["info"]["private"] = 1 + logger.debug("private parameter triggered") + if source: + self.meta["info"]["source"] = source + logger.debug("source parameter found %s", source) + if url_list: + self.meta["url-list"] = url_list + logger.debug("url list parameter found %s", str(url_list)) + if httpseeds: + self.meta["httpseeds"] = httpseeds + logger.debug("httpseeds parameter found %s", str(httpseeds)) + self.meta["info"]["piece length"] = self.piece_length + self.meta_version = meta_version + parent, self.name = os.path.split(self.path) + if not self.name: + self.name = os.path.basename(parent) + self.meta["info"]["name"] = self.name +
+
+
+
+
+
-
+

+assemble() -

-__init__(self, paths, piece_length, progress=True) - - special - +
-

-

Generate hashes of piece length data from filelist contents.

+

Overload in subclasses.

+ +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ Exception +

NotImplementedError

Source code in torrentfile\torrent.py -
def __init__(self, paths: list, piece_length: int, progress: bool = True):
-    """Generate hashes of piece length data from filelist contents."""
-    self.piece_length = piece_length
-    self.paths = paths
-    self.progress = progress
-    self.total = sum([os.path.getsize(i) for i in self.paths])
-    self.index = 0
-    self.current = open(self.paths[0], "rb")
-    if self.progress:
-        total = os.path.getsize(self.paths[0])
-        self.prog_start(total, self.paths[0], unit="bytes")
-    logger.debug("Hashing %s", str(self.paths[0]))
+          
350
+351
+352
+353
+354
+355
+356
+357
+358
+359
def assemble(self):
+    """
+    Overload in subclasses.
+
+    Raises
+    ------
+    Exception
+        NotImplementedError
+    """
+    raise NotImplementedError
 
+
@@ -21494,51 +31966,75 @@
+
-
-__iter__(self) +

+set_callback(func) - special + classmethod -

+
+
-

Iterate through feed pieces.

+

Assign a callback function for the Hashing class to call for each hash.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
func + function +

The callback function which accepts a single paramter.

+ required +
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
iterator

Iterator for leaves/hash pieces.

Source code in torrentfile\torrent.py -
def __iter__(self):
+          
240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
@classmethod
+def set_callback(cls, func):
     """
-    Iterate through feed pieces.
+    Assign a callback function for the Hashing class to call for each hash.
 
-    Returns
-    -------
-    self : iterator
-        Iterator for leaves/hash pieces.
+    Parameters
+    ----------
+    func : function
+        The callback function which accepts a single paramter.
     """
-    return self
+    if "hasher" in vars(cls) and vars(cls)["hasher"]:
+        cls.hasher.set_callback(func)
 
+
@@ -21546,62 +32042,38 @@
+
-
-__next__(self) +

+sort_meta() - - special - -

+
+
-

Generate piece-length pieces of data from input file list.

+

Sort the info and meta dictionaries.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bytes

SHA1 hash of the piece extracted.

Source code in torrentfile\torrent.py -
def __next__(self) -> bytes:
-    """
-    Generate piece-length pieces of data from input file list.
-
-    Returns
-    -------
-    bytes
-        SHA1 hash of the piece extracted.
-    """
-    while True:
-        piece = bytearray(self.piece_length)
-        size = self.current.readinto(piece)
-        if size == 0:
-            if not self.next_file():
-                raise StopIteration
-        elif size < self.piece_length:
-            self.prog_update(size)
-            return self._handle_partial(piece[:size])
-        else:
-            self.prog_update(size)
-            return sha1(piece).digest()  # nosec
+          
361
+362
+363
+364
+365
+366
+367
def sort_meta(self):
+    """Sort the info and meta dictionaries."""
+    logger.debug("sorting dictionary keys")
+    meta = self.meta
+    meta["info"] = dict(sorted(list(meta["info"].items())))
+    meta = dict(sorted(list(meta.items())))
+    return meta
 
+
@@ -21609,58 +32081,140 @@
+
+ +

+write(outfile=None) -

-next_file(self) +
-
-

Seemlessly transition to next file in file list.

+

Write meta information to .torrent file.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
outfile + str +

Destination path for .torrent file. default=None

+ None +
+ +

Returns:

+ + + + + + + + + + + + + + + + + +
Name TypeDescription
outfile + str +

Where the .torrent file was writen.

meta + dict +

.torrent meta information.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
bool

True if there is a next file otherwise False.

Source code in torrentfile\torrent.py -
def next_file(self) -> bool:
+          
369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
+402
def write(self, outfile=None) -> tuple:
     """
-    Seemlessly transition to next file in file list.
+    Write meta information to .torrent file.
+
+    Parameters
+    ----------
+    outfile : str
+        Destination path for .torrent file. default=None
 
     Returns
     -------
-    bool:
-        True if there is a next file otherwise False.
+    outfile : str
+        Where the .torrent file was writen.
+    meta : dict
+        .torrent meta information.
     """
-    self.index += 1
-    self.prog_close()
-    if self.index < len(self.paths):
-        path = self.paths[self.index]
-        logger.debug("Hashing %s", str(path))
-        self.current.close()
-        if self.progress:
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
-        self.current = open(path, "rb")
-        return True
-    return False
+    fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
+    if not self.outfile and not outfile:
+        if self.cwd:
+            self.outfile = fallback
+        else:
+            path = str(self.path).rstrip("\\/")
+            self.outfile = path + ".torrent"
+    elif outfile:
+        self.outfile = outfile
+    if str(self.outfile)[-1] in "\\/":
+        self.outfile = self.outfile + (self.name + ".torrent")
+    self.meta = self.sort_meta()
+    try:
+        pyben.dump(self.meta, self.outfile)
+    except PermissionError:
+        self.outfile = fallback
+        pyben.dump(self.meta, fallback)
+    return self.outfile, self.meta
 
+
@@ -21678,285 +32232,536 @@
-
+

+ TorrentAssembler -

-__init__(self, **kwargs) - - special - +

-
+

+ Bases: MetaFile

-

Construct TorrentFile instance with given keyword args.

-

Parameters:

- - - - - - - - - - +

Assembler class for Bittorrent version 2 and hybrid meta files.

+

This differs from the TorrentFileV2 and TorrentFileHybrid, because +it can be used as an iterator and works for both versions.

+ +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
kwargsdict

dictionary of keyword args passed to superclass.

{}NameTypeDescriptionDefault
-
- Source code in torrentfile\torrent.py -
def __init__(self, **kwargs):
+    
+    
+        
+          kwargs
+          
+                dict
+          
+          

Keyword arguments for torrent options.

+ + required + + + + + + +
+ Source code in torrentfile\torrent.py +
643
+644
+645
+646
+647
+648
+649
+650
+651
+652
+653
+654
+655
+656
+657
+658
+659
+660
+661
+662
+663
+664
+665
+666
+667
+668
+669
+670
+671
+672
+673
+674
+675
+676
+677
+678
+679
+680
+681
+682
+683
+684
+685
+686
+687
+688
+689
+690
+691
+692
+693
+694
+695
+696
+697
+698
+699
+700
+701
+702
+703
+704
+705
+706
+707
+708
+709
+710
+711
+712
+713
+714
+715
+716
+717
+718
+719
+720
+721
+722
+723
+724
+725
+726
+727
+728
+729
+730
+731
+732
+733
+734
+735
+736
+737
+738
+739
class TorrentAssembler(MetaFile):
     """
-    Construct TorrentFile instance with given keyword args.
+    Assembler class for Bittorrent version 2 and hybrid meta files.
+
+    This differs from the TorrentFileV2 and TorrentFileHybrid, because
+    it can be used as an iterator and works for both versions.
 
     Parameters
     ----------
     kwargs : dict
-        dictionary of keyword args passed to superclass.
+        Keyword arguments for torrent options.
     """
-    super().__init__(**kwargs)
-    logger.debug("Assembling bittorrent v1 torrent file")
-    self.assemble()
+
+    hasher = FileHasher
+
+    def __init__(self, **kwargs):
+        """
+        Create Bittorrent v1 v2 hybrid metafiles.
+        """
+        super().__init__(**kwargs)
+        logger.debug("Assembling bittorrent Hybrid file")
+        self.name = os.path.basename(self.path)
+        self.hashes = []
+        self.piece_layers = {}
+        self.pieces = bytearray()
+        self.files = []
+        self.hybrid = self.meta_version == "3"
+        self.total = len(utils.get_file_list(self.path))
+        self.assemble()
+
+    def assemble(self):
+        """
+        Assemble the parts of the torrentfile into meta dictionary.
+        """
+        info = self.meta["info"]
+        info["meta version"] = 2
+
+        if os.path.isfile(self.path):
+            info["file tree"] = {self.name: self._traverse(self.path)}
+            info["length"] = os.path.getsize(self.path)
+
+        else:
+            info["file tree"] = self._traverse(self.path)
+            if self.hybrid:
+                info["files"] = self.files
+
+        if self.hybrid:
+            info["pieces"] = self.pieces
+        self.meta["piece layers"] = self.piece_layers
+        return info
+
+    def _traverse(self, path: str) -> dict:
+        """
+        Build meta dictionary while walking directory.
+
+        Parameters
+        ----------
+        path : str
+            Path to target file.
+        """
+        if os.path.isfile(path):
+            file_size = os.path.getsize(path)
+            if self.hybrid:
+                self.files.append(
+                    {
+                        "length": file_size,
+                        "path": os.path.relpath(path, self.path).split(os.sep),
+                    }
+                )
+
+            if file_size == 0:
+                return {"": {"length": file_size}}
+
+            logger.debug("Hashing %s", str(path))
+            hasher = FileHasher(
+                path, self.piece_length, progress=True, hybrid=self.hybrid
+            )
+            layers = bytearray()
+            for result in hasher:
+                if self.hybrid:
+                    layer_hash, piece = result
+                    self.pieces.extend(piece)
+                else:
+                    layer_hash = result
+                layers.extend(layer_hash)
+            if file_size > self.piece_length:
+                self.piece_layers[hasher.root] = layers
+            if self.hybrid and hasher.padding_file:
+                self.files.append(hasher.padding_file)
+
+            return {"": {"length": file_size, "pieces root": hasher.root}}
+
+        tree = {}
+        if os.path.isdir(path):
+            for name in sorted(os.listdir(path)):
+                tree[name] = self._traverse(os.path.join(path, name))
+        return tree
 
- - +
+
-
+
-
-

-assemble(self) -

-
-

Assemble components of torrent metafile.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
dict

metadata dictionary for torrent file

-
- Source code in torrentfile\torrent.py -
def assemble(self):
-    """
-    Assemble components of torrent metafile.
 
-    Returns
-    -------
-    dict
-        metadata dictionary for torrent file
-    """
-    info = self.meta["info"]
-    size, filelist = utils.filelist_total(self.path)
-    if os.path.isfile(self.path):
-        info["length"] = size
-    else:
-        info["files"] = [
-            {
-                "length": os.path.getsize(path),
-                "path": os.path.relpath(path, self.path).split(os.sep),
-            }
-            for path in filelist
-        ]
-    pieces = bytearray()
 
-    feeder = Hasher(filelist, self.piece_length, self.progress)
-    for piece in feeder:
-        pieces.extend(piece)
 
-    info["pieces"] = pieces
-
-
-
-
+
-
+

+__init__(**kwargs) + + +

+ + +
+ +

Create Bittorrent v1 v2 hybrid metafiles.

+ +
+ Source code in torrentfile\torrent.py +
658
+659
+660
+661
+662
+663
+664
+665
+666
+667
+668
+669
+670
+671
def __init__(self, **kwargs):
+    """
+    Create Bittorrent v1 v2 hybrid metafiles.
+    """
+    super().__init__(**kwargs)
+    logger.debug("Assembling bittorrent Hybrid file")
+    self.name = os.path.basename(self.path)
+    self.hashes = []
+    self.piece_layers = {}
+    self.pieces = bytearray()
+    self.files = []
+    self.hybrid = self.meta_version == "3"
+    self.total = len(utils.get_file_list(self.path))
+    self.assemble()
+
+
+
-
+
-

- -TorrentFileHybrid (MetaFile, ProgMixin) - +

+_traverse(path) +

-
-

Construct the Hybrid torrent meta file with provided parameters.

-

DEPRECATED

+

Build meta dictionary while walking directory.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
kwargsdict

Keyword arguments for torrent options.

{}NameTypeDescriptionDefault
+ + + + path + + str + +

Path to target file.

+ + required + + + + +
Source code in torrentfile\torrent.py -
class TorrentFileHybrid(MetaFile, ProgMixin):
-    """
-    Construct the Hybrid torrent meta file with provided parameters.
-
-    **DEPRECATED**
+          
694
+695
+696
+697
+698
+699
+700
+701
+702
+703
+704
+705
+706
+707
+708
+709
+710
+711
+712
+713
+714
+715
+716
+717
+718
+719
+720
+721
+722
+723
+724
+725
+726
+727
+728
+729
+730
+731
+732
+733
+734
+735
+736
+737
+738
+739
def _traverse(self, path: str) -> dict:
+    """
+    Build meta dictionary while walking directory.
 
     Parameters
     ----------
-    kwargs : dict
-        Keyword arguments for torrent options.
+    path : str
+        Path to target file.
     """
+    if os.path.isfile(path):
+        file_size = os.path.getsize(path)
+        if self.hybrid:
+            self.files.append(
+                {
+                    "length": file_size,
+                    "path": os.path.relpath(path, self.path).split(os.sep),
+                }
+            )
 
-    hasher = HasherHybrid
+        if file_size == 0:
+            return {"": {"length": file_size}}
 
-    def __init__(self, **kwargs):
-        """
-        Create Bittorrent v1 v2 hybrid metafiles.
-        """
-        super().__init__(**kwargs)
-        logger.debug("Assembling bittorrent Hybrid file")
-        self.name = os.path.basename(self.path)
-        self.hashes = []
-        self.piece_layers = {}
-        self.pieces = []
-        self.files = []
-        self.total = len(utils.get_file_list(self.path))
-        self.assemble()
+        logger.debug("Hashing %s", str(path))
+        hasher = FileHasher(
+            path, self.piece_length, progress=True, hybrid=self.hybrid
+        )
+        layers = bytearray()
+        for result in hasher:
+            if self.hybrid:
+                layer_hash, piece = result
+                self.pieces.extend(piece)
+            else:
+                layer_hash = result
+            layers.extend(layer_hash)
+        if file_size > self.piece_length:
+            self.piece_layers[hasher.root] = layers
+        if self.hybrid and hasher.padding_file:
+            self.files.append(hasher.padding_file)
 
-    def assemble(self):
-        """
-        Assemble the parts of the torrentfile into meta dictionary.
+        return {"": {"length": file_size, "pieces root": hasher.root}}
 
-        **DEPRECATED**
-        """
-        info = self.meta["info"]
-        info["meta version"] = 2
+    tree = {}
+    if os.path.isdir(path):
+        for name in sorted(os.listdir(path)):
+            tree[name] = self._traverse(os.path.join(path, name))
+    return tree
+
+
+
+
- if os.path.isfile(self.path): - info["file tree"] = {self.name: self._traverse(self.path)} - info["length"] = os.path.getsize(self.path) +
- else: - info["file tree"] = self._traverse(self.path) - info["files"] = self.files - info["pieces"] = b"".join(self.pieces) - self.meta["piece layers"] = self.piece_layers - return info - def _traverse(self, path: str) -> dict: - """ - Build meta dictionary while walking directory. +
- **DEPRECATED** - Parameters - ---------- - path : str - Path to target file. - """ - if os.path.isfile(path): - file_size = os.path.getsize(path) - self.files.append( - { - "length": file_size, - "path": os.path.relpath(path, self.path).split(os.sep), - } - ) +

+assemble() - if file_size == 0: - return {"": {"length": file_size}} - logger.debug("Hashing %s", str(path)) - file_hash = HasherHybrid(path, self.piece_length, self.progress) - self.prog_update(file_size) +

- if file_size > self.piece_length: - self.piece_layers[file_hash.root] = file_hash.piece_layer - self.hashes.append(file_hash) - self.pieces.extend(file_hash.pieces) +
- if file_hash.padding_file: - self.files.append(file_hash.padding_file) +

Assemble the parts of the torrentfile into meta dictionary.

- return {"": {"length": file_size, "pieces root": file_hash.root}} +
+ Source code in torrentfile\torrent.py +
673
+674
+675
+676
+677
+678
+679
+680
+681
+682
+683
+684
+685
+686
+687
+688
+689
+690
+691
+692
def assemble(self):
+    """
+    Assemble the parts of the torrentfile into meta dictionary.
+    """
+    info = self.meta["info"]
+    info["meta version"] = 2
 
-        tree = {}
-        if os.path.isdir(path):
-            for name in sorted(os.listdir(path)):
-                tree[name] = self._traverse(os.path.join(path, name))
-        return tree
+    if os.path.isfile(self.path):
+        info["file tree"] = {self.name: self._traverse(self.path)}
+        info["length"] = os.path.getsize(self.path)
+
+    else:
+        info["file tree"] = self._traverse(self.path)
+        if self.hybrid:
+            info["files"] = self.files
+
+    if self.hybrid:
+        info["pieces"] = self.pieces
+    self.meta["piece layers"] = self.piece_layers
+    return info
 
+
+
+ +
-
+
+
+
@@ -21964,184 +32769,161 @@

-

- -hasher (CbMixin, ProgMixin) - +

+ TorrentFile + +

-
+

+ Bases: MetaFile, ProgMixin

-

Calculate root and piece hashes for creating hybrid torrent file.

-

DEPRECATED

-

Create merkle tree layers from sha256 hashed 16KiB blocks of contents. -With a branching factor of 2, merge layer hashes until blocks equal -piece_length bytes for the piece layer, and then the root hash.

-

Parameters:

- - - - - - - - - - - - - - - - - - - - - - +

Class for creating Bittorrent meta files.

+

Construct Torrentfile class instance object.

+ +

Parameters:

+
NameTypeDescriptionDefault
pathstr

path to target file.

required
piece_lengthint

piece length for data chunks.

required
+ - - - - + + + + - -
progressbool

default = None

TrueNameTypeDescriptionDefault
-
- Source code in torrentfile\torrent.py -
class HasherHybrid(CbMixin, ProgMixin):
+    
+    
+        
+          kwargs
+          
+                dict
+          
+          

Dictionary containing torrent file options.

+ + required + + + + + + +
+ Source code in torrentfile\torrent.py +
405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
class TorrentFile(MetaFile, ProgMixin):
     """
-    Calculate root and piece hashes for creating hybrid torrent file.
-
-    **DEPRECATED**
+    Class for creating Bittorrent meta files.
 
-    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
-    With a branching factor of 2, merge layer hashes until blocks equal
-    piece_length bytes for the piece layer, and then the root hash.
+    Construct *Torrentfile* class instance object.
 
     Parameters
     ----------
-    path : str
-        path to target file.
-    piece_length : int
-        piece length for data chunks.
-    progress : int
-        default = None
-    """
-
-    def __init__(self, path: str, piece_length: int, progress: bool = True):
-        """
-        Construct Hasher class instances for each file in torrent.
-
-        **DEPRECATED**
-        """
-        self.path = path
-        self.piece_length = piece_length
-        self.pieces = []
-        self.layer_hashes = []
-        self.piece_layer = None
-        self.root = None
-        self.padding_piece = None
-        self.padding_file = None
-        if progress:
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
-        self.amount = piece_length // BLOCK_SIZE
-        with open(path, "rb") as data:
-            self.process_file(data)
-
-    def _pad_remaining(self, block_count: int):
-        """
-        Generate Hash sized, 0 filled bytes for padding.
-
-        **DEPRECATED**
-
-        Parameters
-        ----------
-        block_count : int
-            current total number of blocks collected.
-
-        Returns
-        -------
-        padding : bytes
-            Padding to fill remaining portion of tree.
-        """
-        # when the there is only one block for file
-        remaining = self.amount - block_count
-        if not self.layer_hashes:
-            power2 = next_power_2(block_count)
-            remaining = power2 - block_count
-        self.prog_update(HASH_SIZE * remaining)
-        return [bytes(HASH_SIZE) for _ in range(remaining)]
-
-    def process_file(self, data: bytearray):
-        """
-        Calculate layer hashes for contents of file.
-
-        **DEPRECATED**
-
-        Parameters
-        ----------
-        data : BytesIO
-            File opened in read mode.
-        """
-        while True:
-            plength = self.piece_length
-            blocks = []
-            piece = sha1()  # nosec
-            total = 0
-            block = bytearray(BLOCK_SIZE)
-            for _ in range(self.amount):
-                size = data.readinto(block)
-                self.prog_update(size)
-                if not size:
-                    break
-                total += size
-                plength -= size
-                blocks.append(sha256(block[:size]).digest())
-                piece.update(block[:size])
-            if not blocks:
-                break
-            if len(blocks) != self.amount:
-                padding = self._pad_remaining(len(blocks))
-                blocks.extend(padding)
-            layer_hash = merkle_root(blocks)
-            if self._cb:
-                self._cb(layer_hash)
-            self.layer_hashes.append(layer_hash)
-            if plength > 0:
-                self.padding_file = {
-                    "attr": "p",
-                    "length": plength,
-                    "path": [".pad", str(plength)],
-                }
-                piece.update(bytes(plength))
-            self.pieces.append(piece.digest())  # nosec
-        self._calculate_root()
-        self.prog_close()
+    kwargs : dict
+        Dictionary containing torrent file options.
+    """
 
-    def _calculate_root(self):
+    hasher = Hasher
+
+    def __init__(self, **kwargs):
         """
-        Calculate the root hash for opened file.
+        Construct TorrentFile instance with given keyword args.
 
-        **DEPRECATED**
+        Parameters
+        ----------
+        kwargs : dict
+            dictionary of keyword args passed to superclass.
         """
-        self.piece_layer = b"".join(self.layer_hashes)
+        super().__init__(**kwargs)
+        logger.debug("Assembling bittorrent v1 torrent file")
+        self.assemble()
 
-        if len(self.layer_hashes) > 1:
-            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
+    def assemble(self):
+        """
+        Assemble components of torrent metafile.
 
-            pow2 = next_power_2(len(self.layer_hashes))
-            remainder = pow2 - len(self.layer_hashes)
+        Returns
+        -------
+        dict
+            metadata dictionary for torrent file
+        """
+        info = self.meta["info"]
+        size, filelist = utils.filelist_total(self.path)
+        if os.path.isfile(self.path):
+            info["length"] = size
+        else:
+            info["files"] = [
+                {
+                    "length": os.path.getsize(path),
+                    "path": os.path.relpath(path, self.path).split(os.sep),
+                }
+                for path in filelist
+            ]
+        pieces = bytearray()
 
-            self.layer_hashes += [pad_piece for _ in range(remainder)]
-        self.root = merkle_root(self.layer_hashes)
+        feeder = Hasher(filelist, self.piece_length, self.progress)
+        for piece in feeder:
+            pieces.extend(piece)
+
+        info["pieces"] = pieces
 
- +
+
@@ -22155,46 +32937,72 @@

-
+
-
-__init__(self, path, piece_length, progress=True) +

+__init__(**kwargs) - - special - -

+

+
-

Construct Hasher class instances for each file in torrent.

-

DEPRECATED

+

Construct TorrentFile instance with given keyword args.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
kwargs + dict +

dictionary of keyword args passed to superclass.

+ required +
Source code in torrentfile\torrent.py -
def __init__(self, path: str, piece_length: int, progress: bool = True):
+          
419
+420
+421
+422
+423
+424
+425
+426
+427
+428
+429
+430
def __init__(self, **kwargs):
     """
-    Construct Hasher class instances for each file in torrent.
+    Construct TorrentFile instance with given keyword args.
 
-    **DEPRECATED**
+    Parameters
+    ----------
+    kwargs : dict
+        dictionary of keyword args passed to superclass.
     """
-    self.path = path
-    self.piece_length = piece_length
-    self.pieces = []
-    self.layer_hashes = []
-    self.piece_layer = None
-    self.root = None
-    self.padding_piece = None
-    self.padding_file = None
-    if progress:
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
-    self.amount = piece_length // BLOCK_SIZE
-    with open(path, "rb") as data:
-        self.process_file(data)
+    super().__init__(**kwargs)
+    logger.debug("Assembling bittorrent v1 torrent file")
+    self.assemble()
 
+
@@ -22202,127 +33010,399 @@
+
-
-process_file(self, data) +

+assemble() -

+
+
-

Calculate layer hashes for contents of file.

-

DEPRECATED

+

Assemble components of torrent metafile.

-

Parameters:

- - - - - - - - - - +

Returns:

+
NameTypeDescriptionDefault
+ - - - - + + - -
databytearray

File opened in read mode.

requiredTypeDescription
+ + + + + dict + +

metadata dictionary for torrent file

+ + + +
Source code in torrentfile\torrent.py -
def process_file(self, data: bytearray):
+          
432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
+449
+450
+451
+452
+453
+454
+455
+456
+457
+458
+459
def assemble(self):
     """
-    Calculate layer hashes for contents of file.
+    Assemble components of torrent metafile.
+
+    Returns
+    -------
+    dict
+        metadata dictionary for torrent file
+    """
+    info = self.meta["info"]
+    size, filelist = utils.filelist_total(self.path)
+    if os.path.isfile(self.path):
+        info["length"] = size
+    else:
+        info["files"] = [
+            {
+                "length": os.path.getsize(path),
+                "path": os.path.relpath(path, self.path).split(os.sep),
+            }
+            for path in filelist
+        ]
+    pieces = bytearray()
+
+    feeder = Hasher(filelist, self.piece_length, self.progress)
+    for piece in feeder:
+        pieces.extend(piece)
+
+    info["pieces"] = pieces
+
+
+
+
+ +
+ + + + + +
+ +
+ +
+ + + +
+ + + +

+ TorrentFileHybrid + + + +

+ + +
+

+ Bases: MetaFile, ProgMixin

+ + +

Construct the Hybrid torrent meta file with provided parameters.

+

DEPRECATED

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
kwargs + dict +

Keyword arguments for torrent options.

+ required +
+ + +
+ Source code in torrentfile\torrent.py +
548
+549
+550
+551
+552
+553
+554
+555
+556
+557
+558
+559
+560
+561
+562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
+575
+576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
+596
+597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
class TorrentFileHybrid(MetaFile, ProgMixin):
+    """
+    Construct the Hybrid torrent meta file with provided parameters.
 
     **DEPRECATED**
 
     Parameters
     ----------
-    data : BytesIO
-        File opened in read mode.
+    kwargs : dict
+        Keyword arguments for torrent options.
     """
-    while True:
-        plength = self.piece_length
-        blocks = []
-        piece = sha1()  # nosec
-        total = 0
-        block = bytearray(BLOCK_SIZE)
-        for _ in range(self.amount):
-            size = data.readinto(block)
-            self.prog_update(size)
-            if not size:
-                break
-            total += size
-            plength -= size
-            blocks.append(sha256(block[:size]).digest())
-            piece.update(block[:size])
-        if not blocks:
-            break
-        if len(blocks) != self.amount:
-            padding = self._pad_remaining(len(blocks))
-            blocks.extend(padding)
-        layer_hash = merkle_root(blocks)
-        if self._cb:
-            self._cb(layer_hash)
-        self.layer_hashes.append(layer_hash)
-        if plength > 0:
-            self.padding_file = {
-                "attr": "p",
-                "length": plength,
-                "path": [".pad", str(plength)],
-            }
-            piece.update(bytes(plength))
-        self.pieces.append(piece.digest())  # nosec
-    self._calculate_root()
-    self.prog_close()
+
+    hasher = HasherHybrid
+
+    def __init__(self, **kwargs):
+        """
+        Create Bittorrent v1 v2 hybrid metafiles.
+        """
+        super().__init__(**kwargs)
+        logger.debug("Assembling bittorrent Hybrid file")
+        self.name = os.path.basename(self.path)
+        self.hashes = []
+        self.piece_layers = {}
+        self.pieces = []
+        self.files = []
+        self.total = len(utils.get_file_list(self.path))
+        self.assemble()
+
+    def assemble(self):
+        """
+        Assemble the parts of the torrentfile into meta dictionary.
+
+        **DEPRECATED**
+        """
+        info = self.meta["info"]
+        info["meta version"] = 2
+
+        if os.path.isfile(self.path):
+            info["file tree"] = {self.name: self._traverse(self.path)}
+            info["length"] = os.path.getsize(self.path)
+
+        else:
+            info["file tree"] = self._traverse(self.path)
+            info["files"] = self.files
+
+        info["pieces"] = b"".join(self.pieces)
+        self.meta["piece layers"] = self.piece_layers
+        return info
+
+    def _traverse(self, path: str) -> dict:
+        """
+        Build meta dictionary while walking directory.
+
+        **DEPRECATED**
+
+        Parameters
+        ----------
+        path : str
+            Path to target file.
+        """
+        if os.path.isfile(path):
+            file_size = os.path.getsize(path)
+
+            self.files.append(
+                {
+                    "length": file_size,
+                    "path": os.path.relpath(path, self.path).split(os.sep),
+                }
+            )
+
+            if file_size == 0:
+                return {"": {"length": file_size}}
+
+            logger.debug("Hashing %s", str(path))
+            file_hash = HasherHybrid(path, self.piece_length, self.progress)
+            self.prog_update(file_size)
+
+            if file_size > self.piece_length:
+                self.piece_layers[file_hash.root] = file_hash.piece_layer
+
+            self.hashes.append(file_hash)
+            self.pieces.extend(file_hash.pieces)
+
+            if file_hash.padding_file:
+                self.files.append(file_hash.padding_file)
+
+            return {"": {"length": file_size, "pieces root": file_hash.root}}
+
+        tree = {}
+        if os.path.isdir(path):
+            for name in sorted(os.listdir(path)):
+                tree[name] = self._traverse(os.path.join(path, name))
+        return tree
 
- - +
+
+ -
+
-
-
-
-
+ + + + +

-__init__(self, **kwargs) +__init__(**kwargs) - - special -

+

Create Bittorrent v1 v2 hybrid metafiles.

Source code in torrentfile\torrent.py -
def __init__(self, **kwargs):
+          
562
+563
+564
+565
+566
+567
+568
+569
+570
+571
+572
+573
+574
def __init__(self, **kwargs):
     """
     Create Bittorrent v1 v2 hybrid metafiles.
     """
@@ -22336,6 +33416,145 @@ 

self.total = len(utils.get_file_list(self.path)) self.assemble()

+
+
+
+ +
+ + + +
+ + + +

+_traverse(path) + + +

+ + +
+ +

Build meta dictionary while walking directory.

+

DEPRECATED

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

Path to target file.

+ required +
+ +
+ Source code in torrentfile\torrent.py +
597
+598
+599
+600
+601
+602
+603
+604
+605
+606
+607
+608
+609
+610
+611
+612
+613
+614
+615
+616
+617
+618
+619
+620
+621
+622
+623
+624
+625
+626
+627
+628
+629
+630
+631
+632
+633
+634
+635
+636
+637
+638
+639
+640
def _traverse(self, path: str) -> dict:
+    """
+    Build meta dictionary while walking directory.
+
+    **DEPRECATED**
+
+    Parameters
+    ----------
+    path : str
+        Path to target file.
+    """
+    if os.path.isfile(path):
+        file_size = os.path.getsize(path)
+
+        self.files.append(
+            {
+                "length": file_size,
+                "path": os.path.relpath(path, self.path).split(os.sep),
+            }
+        )
+
+        if file_size == 0:
+            return {"": {"length": file_size}}
+
+        logger.debug("Hashing %s", str(path))
+        file_hash = HasherHybrid(path, self.piece_length, self.progress)
+        self.prog_update(file_size)
+
+        if file_size > self.piece_length:
+            self.piece_layers[file_hash.root] = file_hash.piece_layer
+
+        self.hashes.append(file_hash)
+        self.pieces.extend(file_hash.pieces)
+
+        if file_hash.padding_file:
+            self.files.append(file_hash.padding_file)
+
+        return {"": {"length": file_size, "pieces root": file_hash.root}}
+
+    tree = {}
+    if os.path.isdir(path):
+        for name in sorted(os.listdir(path)):
+            tree[name] = self._traverse(os.path.join(path, name))
+    return tree
+
+
@@ -22343,16 +33562,17 @@

-
+

-assemble(self) +assemble()

+

Assemble the parts of the torrentfile into meta dictionary.

@@ -22360,7 +33580,26 @@

Source code in torrentfile\torrent.py -
def assemble(self):
+          
576
+577
+578
+579
+580
+581
+582
+583
+584
+585
+586
+587
+588
+589
+590
+591
+592
+593
+594
+595
def assemble(self):
     """
     Assemble the parts of the torrentfile into meta dictionary.
 
@@ -22381,6 +33620,7 @@ 

self.meta["piece layers"] = self.piece_layers return info

+

@@ -22403,41 +33643,132 @@

- -TorrentFileV2 (MetaFile, ProgMixin) - + TorrentFileV2

+
+

+ Bases: MetaFile, ProgMixin

+

Class for creating Bittorrent meta v2 files.

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
kwargsdict

Keyword arguments for torrent file options.

{}NameTypeDescriptionDefault
-
- Source code in torrentfile\torrent.py -
class TorrentFileV2(MetaFile, ProgMixin):
+    
+    
+        
+          kwargs
+          
+                dict
+          
+          

Keyword arguments for torrent file options.

+ + required + + + + + + +
+ Source code in torrentfile\torrent.py +
462
+463
+464
+465
+466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
+493
+494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
+515
+516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
class TorrentFileV2(MetaFile, ProgMixin):
     """
     Class for creating Bittorrent meta v2 files.
 
@@ -22504,335 +33835,119 @@ 

""" if os.path.isfile(path): # Calculate Size and hashes for each file. - size = os.path.getsize(path) - - if size == 0: - return {"": {"length": size}} - - logger.debug("Hashing %s", str(path)) - fhash = HasherV2(path, self.piece_length, self.progress) - - if size > self.piece_length: - self.piece_layers[fhash.root] = fhash.piece_layer - return {"": {"length": size, "pieces root": fhash.root}} - - file_tree = {} - if os.path.isdir(path): - for name in sorted(os.listdir(path)): - file_tree[name] = self._traverse(os.path.join(path, name)) - return file_tree -

- - - - -
- - - - - - - -
- - - -

- -hasher (CbMixin, ProgMixin) - - - - -

- -
- -

Calculate the root hash and piece layers for file contents.

-

DEPRECATED

-

Iterates over 16KiB blocks of data from given file, hashes the data, -then creates a hash tree from the individual block hashes until size of -hashed data equals the piece-length. Then continues the hash tree until -root hash is calculated.

- -

Parameters:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
pathstr

Path to file.

required
piece_lengthint

Size of layer hashes pieces.

required
progressbool

default = None

True
-
- Source code in torrentfile\torrent.py -
class HasherV2(CbMixin, ProgMixin):
-    """
-    Calculate the root hash and piece layers for file contents.
-
-    **DEPRECATED**
-
-    Iterates over 16KiB blocks of data from given file, hashes the data,
-    then creates a hash tree from the individual block hashes until size of
-    hashed data equals the piece-length.  Then continues the hash tree until
-    root hash is calculated.
-
-    Parameters
-    ----------
-    path : str
-        Path to file.
-    piece_length : int
-        Size of layer hashes pieces.
-    progress : int
-        default = None
-    """
-
-    def __init__(self, path: str, piece_length: int, progress: bool = True):
-        """
-        Calculate and store hash information for specific file.
-
-        **DEPRECATED**
-        """
-        self.path = path
-        self.root = None
-        self.piece_layer = None
-        self.layer_hashes = []
-        self.piece_length = piece_length
-        self.num_blocks = piece_length // BLOCK_SIZE
-        if progress:
-            self.prog_start(os.path.getsize(path), path, unit="bytes")
-        with open(self.path, "rb") as fd:
-            self.process_file(fd)
-
-    def process_file(self, fd: str):
-        """
-        Calculate hashes over 16KiB chuncks of file content.
-
-        **DEPRECATED**
-
-        Parameters
-        ----------
-        fd : TextIOWrapper
-            Opened file in read mode.
-        """
-        while True:
-            blocks = []
-            leaf = bytearray(BLOCK_SIZE)
-            # generate leaves of merkle tree
-
-            for _ in range(self.num_blocks):
-                size = fd.readinto(leaf)
-                self.prog_update(size)
-                if not size:
-                    break
-                blocks.append(sha256(leaf[:size]).digest())
-
-            # blocks is empty mean eof
-            if not blocks:
-                break
-            if len(blocks) != self.num_blocks:
-                # when size of file doesn't fill the last block
-                # when the file contains multiple pieces
-                remaining = self.num_blocks - len(blocks)
-                if not self.layer_hashes:
-                    # when the there is only one block for file
-                    power2 = next_power_2(len(blocks))
-                    remaining = power2 - len(blocks)
-
-                # pad the the rest with zeroes to fill remaining space.
-                padding = [bytes(32) for _ in range(remaining)]
-                self.prog_update(HASH_SIZE * remaining)
-                blocks.extend(padding)
-            # calculate the root hash for the merkle tree up to piece-length
-
-            layer_hash = merkle_root(blocks)
-            if self._cb:
-                self._cb(layer_hash)
-            self.layer_hashes.append(layer_hash)
-        self._calculate_root()
-        self.prog_close()
-
-    def _calculate_root(self):
-        """
-        Calculate root hash for the target file.
-
-        **DEPRECATED**
-        """
-        self.piece_layer = b"".join(self.layer_hashes)
-        hashes = len(self.layer_hashes)
-        if hashes > 1:
-            pow2 = next_power_2(hashes)
-            remainder = pow2 - hashes
-            pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)]
-            for _ in range(remainder):
-                self.layer_hashes.append(merkle_root(pad_piece))
-        self.root = merkle_root(self.layer_hashes)
-
-
- - - -
- - + size = os.path.getsize(path) + if size == 0: + return {"": {"length": size}} + logger.debug("Hashing %s", str(path)) + fhash = HasherV2(path, self.piece_length, self.progress) + if size > self.piece_length: + self.piece_layers[fhash.root] = fhash.piece_layer + return {"": {"length": size, "pieces root": fhash.root}} + file_tree = {} + if os.path.isdir(path): + for name in sorted(os.listdir(path)): + file_tree[name] = self._traverse(os.path.join(path, name)) + return file_tree +
+
+
-
+
-
-__init__(self, path, piece_length, progress=True) - - special - -
-
-

Calculate and store hash information for specific file.

-

DEPRECATED

-
- Source code in torrentfile\torrent.py -
def __init__(self, path: str, piece_length: int, progress: bool = True):
-    """
-    Calculate and store hash information for specific file.
 
-    **DEPRECATED**
-    """
-    self.path = path
-    self.root = None
-    self.piece_layer = None
-    self.layer_hashes = []
-    self.piece_length = piece_length
-    self.num_blocks = piece_length // BLOCK_SIZE
-    if progress:
-        self.prog_start(os.path.getsize(path), path, unit="bytes")
-    with open(self.path, "rb") as fd:
-        self.process_file(fd)
-
-
-
-
+
-
+

+__init__(**kwargs) -

-process_file(self, fd) +
-

-

Calculate hashes over 16KiB chuncks of file content.

+

Construct TorrentFileV2 Class instance from given parameters.

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
fdstr

Opened file in read mode.

requiredNameTypeDescriptionDefault
+ + + + kwargs + + dict + +

keywword arguments to pass to superclass.

+ + required + + + + +
Source code in torrentfile\torrent.py -
def process_file(self, fd: str):
+          
476
+477
+478
+479
+480
+481
+482
+483
+484
+485
+486
+487
+488
+489
+490
+491
+492
def __init__(self, **kwargs):
     """
-    Calculate hashes over 16KiB chuncks of file content.
+    Construct `TorrentFileV2` Class instance from given parameters.
 
     **DEPRECATED**
 
     Parameters
     ----------
-    fd : TextIOWrapper
-        Opened file in read mode.
+    kwargs : dict
+        keywword arguments to pass to superclass.
     """
-    while True:
-        blocks = []
-        leaf = bytearray(BLOCK_SIZE)
-        # generate leaves of merkle tree
-
-        for _ in range(self.num_blocks):
-            size = fd.readinto(leaf)
-            self.prog_update(size)
-            if not size:
-                break
-            blocks.append(sha256(leaf[:size]).digest())
-
-        # blocks is empty mean eof
-        if not blocks:
-            break
-        if len(blocks) != self.num_blocks:
-            # when size of file doesn't fill the last block
-            # when the file contains multiple pieces
-            remaining = self.num_blocks - len(blocks)
-            if not self.layer_hashes:
-                # when the there is only one block for file
-                power2 = next_power_2(len(blocks))
-                remaining = power2 - len(blocks)
-
-            # pad the the rest with zeroes to fill remaining space.
-            padding = [bytes(32) for _ in range(remaining)]
-            self.prog_update(HASH_SIZE * remaining)
-            blocks.extend(padding)
-        # calculate the root hash for the merkle tree up to piece-length
-
-        layer_hash = merkle_root(blocks)
-        if self._cb:
-            self._cb(layer_hash)
-        self.layer_hashes.append(layer_hash)
-    self._calculate_root()
-    self.prog_close()
+    super().__init__(**kwargs)
+    logger.debug("Assembling bittorrent v2 torrent file")
+    self.piece_layers = {}
+    self.hashes = []
+    self.total = len(utils.get_file_list(self.path))
+    self.assemble()
 
+
@@ -22840,75 +33955,109 @@
+
-

-__init__(self, **kwargs) +

+_traverse(path) - - special -

+
-

Construct TorrentFileV2 Class instance from given parameters.

+

Walk directory tree.

DEPRECATED

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
kwargsdict

keywword arguments to pass to superclass.

{}NameTypeDescriptionDefault
+ + + + path + + str + +

Path to file or directory.

+ + required + + + + +
Source code in torrentfile\torrent.py -
def __init__(self, **kwargs):
-    """
-    Construct `TorrentFileV2` Class instance from given parameters.
+          
516
+517
+518
+519
+520
+521
+522
+523
+524
+525
+526
+527
+528
+529
+530
+531
+532
+533
+534
+535
+536
+537
+538
+539
+540
+541
+542
+543
+544
+545
def _traverse(self, path: str) -> dict:
+    """
+    Walk directory tree.
 
     **DEPRECATED**
 
     Parameters
     ----------
-    kwargs : dict
-        keywword arguments to pass to superclass.
+    path : str
+        Path to file or directory.
     """
-    super().__init__(**kwargs)
-    logger.debug("Assembling bittorrent v2 torrent file")
-    self.piece_layers = {}
-    self.hashes = []
-    self.total = len(utils.get_file_list(self.path))
-    self.assemble()
+    if os.path.isfile(path):
+        # Calculate Size and hashes for each file.
+        size = os.path.getsize(path)
+
+        if size == 0:
+            return {"": {"length": size}}
+
+        logger.debug("Hashing %s", str(path))
+        fhash = HasherV2(path, self.piece_length, self.progress)
+
+        if size > self.piece_length:
+            self.piece_layers[fhash.root] = fhash.piece_layer
+        return {"": {"length": size, "pieces root": fhash.root}}
+
+    file_tree = {}
+    if os.path.isdir(path):
+        for name in sorted(os.listdir(path)):
+            file_tree[name] = self._traverse(os.path.join(path, name))
+    return file_tree
 
+
@@ -22916,39 +34065,63 @@

-
+

-assemble(self) +assemble()

+

Assemble then return the meta dictionary for encoding.

DEPRECATED

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
dict

Metainformation about the torrent.

+

Returns:

+ + + + + + + + + + + + + +
Name TypeDescription
meta + dict +

Metainformation about the torrent.

+
Source code in torrentfile\torrent.py -
def assemble(self):
+          
494
+495
+496
+497
+498
+499
+500
+501
+502
+503
+504
+505
+506
+507
+508
+509
+510
+511
+512
+513
+514
def assemble(self):
     """
     Assemble then return the meta dictionary for encoding.
 
@@ -22970,6 +34143,7 @@ 

info["meta version"] = 2 self.meta["piece layers"] = self.piece_layers

+
@@ -22990,7 +34164,6 @@

-

@@ -23040,39 +34213,83 @@

- -Memo + Memo

+
+

Memoice chache object.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
funcfunction

The function that is being memoized.

requiredNameTypeDescriptionDefault
-
- Source code in torrentfile\utils.py -
class Memo:
+    
+    
+        
+          func
+          
+                function
+          
+          

The function that is being memoized.

+ + required + + + + + + +
+ Source code in torrentfile\utils.py +
41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
class Memo:
     """
     Memoice chache object.
 
@@ -23111,7 +34328,8 @@ 

self.cache[path] = result return result

- +
+
@@ -23125,45 +34343,86 @@

-
+ + +

-__call__(self, path) +__call__(path) - - special -

+

Invoke each time memo function is called.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

The relative or absolute path being used as key in cache dict.

+ required +
+ +

Returns:

+ + - - - - + + - -
pathstr

The relative or absolute path being used as key in cache dict.

requiredName TypeDescription
+ + + +Any + +

The results of calling the function with path.

+ + + +
Source code in torrentfile\utils.py -
def __call__(self, path):
+          
59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
def __call__(self, path):
     """
     Invoke each time memo function is called.
 
@@ -23184,6 +34443,7 @@ 

self.cache[path] = result return result

+
@@ -23191,26 +34451,30 @@

-
+

-__init__(self, func) +__init__(func) - - special -

+

Construct for memoization.

Source code in torrentfile\utils.py -
def __init__(self, func):
+          
51
+52
+53
+54
+55
+56
+57
def __init__(self, func):
     """
     Construct for memoization.
     """
@@ -23218,6 +34482,7 @@ 

self.counter = 0 self.cache = {}

+
@@ -23240,41 +34505,68 @@

- -MissingPathError (Exception) - + MissingPathError

+
+

+ Bases: Exception

+

Path parameter is required to specify target content.

Creating a .torrent file with no contents seems rather silly.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
messagestr

Message for user (optional).

NoneNameTypeDescriptionDefault
-
- Source code in torrentfile\utils.py -
class MissingPathError(Exception):
+    
+    
+        
+          message
+          
+                str
+          
+          

Message for user (optional).

+ + None + + + + + + +
+ Source code in torrentfile\utils.py +
 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
class MissingPathError(Exception):
     """
     Path parameter is required to specify target content.
 
@@ -23295,7 +34587,8 @@ 

self.message = f"Path arguement is missing and required {str(message)}" super().__init__(message)

- +
+
@@ -23309,19 +34602,17 @@

-
+

-__init__(self, message=None) +__init__(message=None) - - special -

+

Raise when creating a meta file without specifying target content.

@@ -23329,7 +34620,14 @@

Source code in torrentfile\utils.py -
def __init__(self, message: str = None):
+          
 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
def __init__(self, message: str = None):
     """
     Raise when creating a meta file without specifying target content.
 
@@ -23338,6 +34636,7 @@ 

self.message = f"Path arguement is missing and required {str(message)}" super().__init__(message)

+

@@ -23360,40 +34659,65 @@

- -PieceLengthValueError (Exception) - + PieceLengthValueError

+
+

+ Bases: Exception

+

Piece Length parameter must equal a perfect power of 2.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
messagestr

Message for user (optional).

NoneNameTypeDescriptionDefault
-
- Source code in torrentfile\utils.py -
class PieceLengthValueError(Exception):
+    
+    
+        
+          message
+          
+                str
+          
+          

Message for user (optional).

+ + None + + + + + + +
+ Source code in torrentfile\utils.py +
103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
class PieceLengthValueError(Exception):
     """
     Piece Length parameter must equal a perfect power of 2.
 
@@ -23412,49 +34736,327 @@ 

self.message = f"Incorrect value for piece length: {str(message)}" super().__init__(message)

+
+
+ + + +
+ + + + + + + + + +
+ + + +

+__init__(message=None) + + +

+ + +
+ +

Raise when creating a meta file with incorrect piece length value.

+

The message argument is a message to pass to Exception base class.

+ +
+ Source code in torrentfile\utils.py +
113
+114
+115
+116
+117
+118
+119
+120
def __init__(self, message: str = None):
+    """
+    Raise when creating a meta file with incorrect piece length value.
+
+    The `message` argument is a message to pass to Exception base class.
+    """
+    self.message = f"Incorrect value for piece length: {str(message)}"
+    super().__init__(message)
+
+
+
+
+ +
+ + + + + +
+ +
+ +
+ + + + +
+ + + +

+_filelist_total(path) + + +

+ + +
+ +

Search directory tree for files.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

Path to file or directory base

+ required +
+ +

Returns:

+ + + + + + + + + + + + + + + + + +
TypeDescription
+ int +

Sum of all filesizes in filelist.

+ list +

All file paths within directory tree.

+ +
+ Source code in torrentfile\utils.py +
228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
def _filelist_total(path: str) -> tuple:
+    """
+    Search directory tree for files.
+
+    Parameters
+    ----------
+    path : str
+        Path to file or directory base
+
+    Returns
+    -------
+    int
+        Sum of all filesizes in filelist.
+    list
+        All file paths within directory tree.
+    """
+    if path.is_file():
+        file_size = os.path.getsize(path)
+        return file_size, [str(path)]
+    total = 0
+    filelist = []
+    if path.is_dir():
+        for item in path.iterdir():
+            size, paths = filelist_total(item)
+            total += size
+            filelist.extend(paths)
+    return total, sorted(filelist)
+
+
+
- - -
- - - - - +
+
-
+

+filelist_total(pathstring) -

-__init__(self, message=None) - - special - +

-

-

Raise when creating a meta file with incorrect piece length value.

-

The message argument is a message to pass to Exception base class.

+

Perform error checking and format conversion to os.PathLike.

+ +

Parameters:

+ + + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
pathstring + str +

An existing filesystem path.

+ required +
+ +

Returns:

+ + + + + + + + + + + + + +
TypeDescription
+ os.PathLike +

Input path converted to bytes format.

+ +

Raises:

+ + + + + + + + + + + + + +
TypeDescription
+ MissingPathError +

File could not be found.

Source code in torrentfile\utils.py -
def __init__(self, message: str = None):
-    """
-    Raise when creating a meta file with incorrect piece length value.
+          
202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
@Memo
+def filelist_total(pathstring: str) -> os.PathLike:
+    """
+    Perform error checking and format conversion to os.PathLike.
 
-    The `message` argument is a message to pass to Exception base class.
+    Parameters
+    ----------
+    pathstring : str
+        An existing filesystem path.
+
+    Returns
+    -------
+    os.PathLike
+        Input path converted to bytes format.
+
+    Raises
+    ------
+    MissingPathError
+        File could not be found.
     """
-    self.message = f"Incorrect value for piece length: {str(message)}"
-    super().__init__(message)
+    if os.path.exists(pathstring):
+        path = Path(pathstring)
+        return _filelist_total(path)
+    raise MissingPathError
 
+
@@ -23462,17 +35064,6 @@

@@ -23483,47 +35074,71 @@

+

Return a sorted list of file paths contained in directory.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

target file or directory.

+ required +
+ +

Returns:

+ + - - - - + + - -
pathstr

target file or directory.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
list

sorted list of file paths.

+ + + + + list + +

sorted list of file paths.

+ + + +
Source code in torrentfile\utils.py -
def get_file_list(path: str) -> list:
+          
275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
def get_file_list(path: str) -> list:
     """
     Return a sorted list of file paths contained in directory.
 
@@ -23540,6 +35155,7 @@ 

_, filelist = filelist_total(path) return filelist

+
@@ -23557,47 +35173,73 @@

+

Calculate the ideal piece length for bittorrent data.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
size + int +

Total bits of all files incluided in .torrent file.

+ required +
+ +

Returns:

+ + - - - - + + - -
sizeint

Total bits of all files incluided in .torrent file.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

Ideal piece length.

+ + + + + int + +

Ideal piece length.

+ + + +
Source code in torrentfile\utils.py -
def get_piece_length(size: int) -> int:
+          
182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
def get_piece_length(size: int) -> int:
     """
     Calculate the ideal piece length for bittorrent data.
 
@@ -23616,6 +35258,7 @@ 

exp += 1 return 2**exp

+
@@ -23633,47 +35276,76 @@

+

Convert integer into human readable memory sized denomination.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
amount + int +

total number of bytes.

+ required +
+ +

Returns:

+ + - - - - + + - -
amountint

total number of bytes.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

human readable representation of the given amount of bytes.

+ + + + + str + +

human readable representation of the given amount of bytes.

+ + + +
Source code in torrentfile\utils.py -
def humanize_bytes(amount: int) -> str:
+          
123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
def humanize_bytes(amount: int) -> str:
     """
     Convert integer into human readable memory sized denomination.
 
@@ -23695,6 +35367,7 @@ 

return f"{amount // 1_048_576} MiB" return f"{amount // 1073741824} GiB"

+
@@ -23712,47 +35385,75 @@

+

Calculate the next perfect power of 2 equal to or greater than value.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
value + int +

integer value that is less than some perfect power of 2.

+ required +
+ +

Returns:

+ + - - - - + + - -
valueint

integer value that is less than some perfect power of 2.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

The next power of 2 greater than value, or value if already power of 2.

+ + + + + int + +

The next power of 2 greater than value, or value if already power of 2.

+ + + +
Source code in torrentfile\utils.py -
def next_power_2(value: int) -> int:
+          
334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
+350
+351
+352
+353
def next_power_2(value: int) -> int:
     """
     Calculate the next perfect power of 2 equal to or greater than value.
 
@@ -23773,6 +35474,7 @@ 

start <<= 1 return start

+
@@ -23790,62 +35492,107 @@

+

Verify input piece_length is valid and convert accordingly.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
piece_length + int | str +

The piece length provided by user.

+ required +
+ +

Returns:

+ + - - - - + + - -
piece_lengthint

The piece length provided by user.

requiredTypeDescription
-

Exceptions:

- - - - - - - - + + + + + + + +
TypeDescription
+ int +

normalized piece length.

+ +

Raises:

+ + - - + + - -
PieceLengthValueError :

If piece length is improper value.

TypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

normalized piece length.

+ + + + + PieceLengthValueError : + +

If piece length is improper value.

+ + + +
Source code in torrentfile\utils.py -
def normalize_piece_length(piece_length: int) -> int:
+          
146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
def normalize_piece_length(piece_length: int) -> int:
     """
     Verify input piece_length is valid and convert accordingly.
 
@@ -23880,6 +35627,7 @@ 

return piece_length raise PieceLengthValueError

+
@@ -23897,47 +35645,71 @@

+

Calculate piece length for input path and contents.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

The absolute path to directory and contents.

+ required +
+ +

Returns:

+ + - - - - + + - -
pathstr

The absolute path to directory and contents.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

The size of pieces of torrent content.

+ + + + + int + +

The size of pieces of torrent content.

+ + + +
Source code in torrentfile\utils.py -
def path_piece_length(path: str) -> int:
+          
316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
+330
+331
def path_piece_length(path: str) -> int:
     """
     Calculate piece length for input path and contents.
 
@@ -23954,6 +35726,7 @@ 

psize = path_size(path) return get_piece_length(psize)

+
@@ -23971,47 +35744,71 @@

+

Return the total size of all files in path recursively.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

path to target file or directory.

+ required +
+ +

Returns:

+ + - - - - + + - -
pathstr

path to target file or directory.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
int

total size of files.

+ + + + + int + +

total size of files.

+ + + +
Source code in torrentfile\utils.py -
def path_size(path: str) -> int:
+          
257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
def path_size(path: str) -> int:
     """
     Return the total size of all files in path recursively.
 
@@ -24028,6 +35825,7 @@ 

total_size, _ = filelist_total(path) return total_size

+
@@ -24045,47 +35843,88 @@

+

Calculate directory statistics.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
path + str +

The path to start calculating from.

+ required +
+ +

Returns:

+ + - - - - + + - -
pathstr

The path to start calculating from.

requiredTypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
tuple

List of all files contained in Directory

+ + + + + list + +

List of all files contained in Directory

+ + + + int + +

Total sum of bytes from all contents of dir

+ + + + int + +

The size of pieces of the torrent contents.

+ + + +
Source code in torrentfile\utils.py -
def path_stat(path: str) -> tuple:
+          
293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
def path_stat(path: str) -> tuple:
     """
     Calculate directory statistics.
 
@@ -24107,6 +35946,7 @@ 

piece_length = get_piece_length(total_size) return (filelist, total_size, piece_length)

+
@@ -24116,7 +35956,6 @@

-

@@ -24154,7 +35993,6 @@

-

@@ -24171,9 +36009,6 @@

tests - - special -

@@ -24202,28 +36037,43 @@

+

Create a specific temporary structured directory.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

path to root of temporary directory

+

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

path to root of temporary directory

+
Source code in tests\__init__.py -
@pytest.fixture(scope="package")
+          
168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
@pytest.fixture(scope="package")
 def dir1():
     """Create a specific temporary structured directory.
 
@@ -24236,6 +36086,7 @@ 

yield root rmpath(root)

+
@@ -24253,28 +36104,43 @@

+

Create a specific temporary structured directory.

-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

path to root of temporary directory

+

Yields:

+ + + + + + + + + + + + + +
TypeDescription
+ str +

path to root of temporary directory

+
Source code in tests\__init__.py -
@pytest.fixture()
+          
182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
@pytest.fixture()
 def dir2():
     """Create a specific temporary structured directory.
 
@@ -24287,6 +36153,7 @@ 

yield Path(root) rmpath(root)

+
@@ -24304,13 +36171,21 @@

+

Return the path to a temporary file package scope.

Source code in tests\__init__.py -
@pytest.fixture(scope="package")
+          
241
+242
+243
+244
+245
+246
+247
+248
@pytest.fixture(scope="package")
 def file1():
     """
     Return the path to a temporary file package scope.
@@ -24319,6 +36194,7 @@ 

yield path rmpath(path)

+
@@ -24336,13 +36212,21 @@

+

Return the path to a temporary file no scope.

Source code in tests\__init__.py -
@pytest.fixture()
+          
297
+298
+299
+300
+301
+302
+303
+304
@pytest.fixture()
 def file2():
     """
     Return the path to a temporary file no scope.
@@ -24351,6 +36235,7 @@ 

yield path rmpath(path)

+
@@ -24368,13 +36253,34 @@

+

Test fixture for generating metafile for all versions of torrents.

Source code in tests\__init__.py -
@pytest.fixture(scope="package", params=torrents())
+          
251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
@pytest.fixture(scope="package", params=torrents())
 def filemeta1(file1, request):
     """
     Test fixture for generating metafile for all versions of torrents.
@@ -24396,6 +36302,7 @@ 

yield outfile rmpath(outfile)

+
@@ -24413,13 +36320,34 @@

+

Test fixture for generating a meta file no scope.

Source code in tests\__init__.py -
@pytest.fixture(params=torrents())
+          
274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
@pytest.fixture(params=torrents())
 def filemeta2(file2, request):
     """
     Test fixture for generating a meta file no scope.
@@ -24441,6 +36369,7 @@ 

yield outfile rmpath(outfile)

+
@@ -24458,13 +36387,34 @@

+

Create a standard metafile for testing.

Source code in tests\__init__.py -
@pytest.fixture(scope="package", params=torrents())
+          
196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
@pytest.fixture(scope="package", params=torrents())
 def metafile1(dir1, request):
     """
     Create a standard metafile for testing.
@@ -24486,6 +36436,7 @@ 

yield outfile rmpath(outfile)

+
@@ -24503,13 +36454,33 @@

+

Create a standard metafile for testing.

Source code in tests\__init__.py -
@pytest.fixture(params=torrents())
+          
219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
@pytest.fixture(params=torrents())
 def metafile2(dir2, request):
     """
     Create a standard metafile for testing.
@@ -24530,6 +36501,7 @@ 

yield outfile rmpath(outfile)

+
@@ -24547,32 +36519,58 @@

+

Remove file or directory path.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
argslist

Filesystem locations for removing.

()NameTypeDescriptionDefault
+ + + + args + + list + +

Filesystem locations for removing.

+ + required + + + + +
Source code in tests\__init__.py -
def rmpath(*args):
+          
 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
def rmpath(*args):
     """Remove file or directory path.
 
     Parameters
@@ -24594,6 +36592,7 @@ 

except PermissionError: # pragma: nocover pass

+
@@ -24611,13 +36610,35 @@

+

Generate variable sized meta files for testing, no scope.

Source code in tests\__init__.py -
@pytest.fixture(params=torrents())
+          
307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
@pytest.fixture(params=torrents())
 def sizedfiles(dir2, sizes, request):
     """
     Generate variable sized meta files for testing, no scope.
@@ -24640,6 +36661,7 @@ 

yield outfile rmpath(outfile)

+
@@ -24657,13 +36679,20 @@

+

Generate powers of 2 for file creation package scope.

Source code in tests\__init__.py -
@pytest.fixture(scope="package", params=[2**i for i in range(15, 20)])
+          
159
+160
+161
+162
+163
+164
+165
@pytest.fixture(scope="package", params=[2**i for i in range(15, 20)])
 def sizes(request):
     """
     Generate powers of 2 for file creation package scope.
@@ -24671,6 +36700,7 @@ 

size = request.param yield size

+
@@ -24688,13 +36718,22 @@

+

Remove all temporary directories and files.

Source code in tests\__init__.py -
@atexit.register
+          
141
+142
+143
+144
+145
+146
+147
+148
+149
@atexit.register
 def teardown():  # pragma: nocover
     """
     Remove all temporary directories and files.
@@ -24704,6 +36743,7 @@ 

if os.path.exists(path): rmpath(path)

+
@@ -24721,47 +36761,91 @@

+

Create temporary directory.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ + + + + + + + + + + + + + + + +
NameTypeDescriptionDefault
ext + str, optional +

extension to file names, by default "1"

+ '1' +
+ +

Returns:

+ + - - - - + + - -
extstr

extension to file names, by default "1"

'1'TypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

path to common root for directory.

+ + + + + str + +

path to common root for directory.

+ + + +
Source code in tests\__init__.py -
def tempdir(ext="1"):
+          
103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
def tempdir(ext="1"):
     """Create temporary directory.
 
     Parameters
@@ -24798,6 +36882,7 @@ 

paths.append(temps) return os.path.commonpath(paths)

+
@@ -24815,54 +36900,108 @@

+

Create temporary file.

Creates a temporary file for unittesting purposes.py

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + + + + + + + + + + + + + + + + +
pathstr

relative path to temporary files, by default None

NoneNameTypeDescriptionDefault
path + str, optional +

relative path to temporary files, by default None

+ None +
exp + int, optional +

Exponent used to determine size of file., by default 18

+ 18 +
+ +

Returns:

+ + - - - - + + - -
expint

Exponent used to determine size of file., by default 18

18TypeDescription
-

Returns:

- - - - - - - - - - - - - -
TypeDescription
str

absolute path to file.

+ + + + + str + +

absolute path to file.

+ + + +
Source code in tests\__init__.py -
def tempfile(path=None, exp=18):
+          
36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
def tempfile(path=None, exp=18):
     """Create temporary file.
 
     Creates a temporary file for unittesting purposes.py
@@ -24905,6 +37044,7 @@ 

os.mkdir(partial) return partial

+
@@ -24922,18 +37062,24 @@

+

Return seq of torrentfile objects.

Source code in tests\__init__.py -
def torrents():
+          
152
+153
+154
+155
+156
def torrents():
     """
     Return seq of torrentfile objects.
     """
     return [TorrentFile, TorrentFileV2, TorrentFileHybrid, TorrentAssembler]
 
+
@@ -24942,7 +37088,6 @@

-
@@ -24979,13 +37124,22 @@

+

Yield a folder object as fixture.

Source code in tests\test_cli.py -
@pytest.fixture(scope="module")
+          
41
+42
+43
+44
+45
+46
+47
+48
+49
@pytest.fixture(scope="module")
 def folder(dir1):
     """
     Yield a folder object as fixture.
@@ -24995,6 +37149,7 @@ 

yield (sfolder, torrent) rmpath(torrent)

+
@@ -25012,13 +37167,35 @@

+

Test announce cli flag.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
+          
122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
 @pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_announce(folder, piece_length, version):
     """
@@ -25041,6 +37218,7 @@ 

meta = pyben.load(torrent) assert meta["announce"] == "https://announce.org/tracker"

+
@@ -25058,13 +37236,37 @@

+

Test announce-list cli flag.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("version", ["1", "2", "3"])
+          
146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
@pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_announce_list(folder, version):
     """
     Test announce-list cli flag.
@@ -25089,6 +37291,7 @@ 

for url in trackers: assert url in [j for i in meta["announce-list"] for j in i]

+
@@ -25106,13 +37309,26 @@

+

Test CLI when path is placed after the trackers flag.

Source code in tests\test_cli.py -
@pytest.mark.parametrize(
+          
451
+452
+453
+454
+455
+456
+457
+458
+459
+460
+461
+462
+463
@pytest.mark.parametrize(
     "flag", ["-t", "-w", "--announce", "--web-seed", "--http-seed"]
 )
 def test_cli_announce_path(dir1, flag):
@@ -25126,6 +37342,7 @@ 

assert os.path.exists(outfile) rmpath(outfile)

+
@@ -25143,13 +37360,38 @@

+

Test comment cli flag.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
+          
172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
 @pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_comment(folder, piece_length, version):
     """
@@ -25175,6 +37417,7 @@ 

meta = pyben.load(torrent) assert meta["info"]["comment"] == "this is a comment"

+
@@ -25192,13 +37435,35 @@

+

Test if created torrents recieve a created by field in meta info.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
+          
254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
 @pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_created_by(folder, piece_length, version):
     """
@@ -25221,6 +37486,7 @@ 

meta = pyben.load(torrent) assert "TorrentFile" in meta["created by"]

+
@@ -25238,13 +37504,40 @@

+

Test if torrents created get an accurate timestamp.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
+          
225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
 @pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_creation_date(folder, piece_length, version):
     """
@@ -25272,6 +37565,7 @@ 

assert date.year == now.year assert date.month == now.month

+
@@ -25289,13 +37583,31 @@

+

Test outfile cli flag.

Source code in tests\test_cli.py -
def test_cli_cwd(folder):
+          
466
+467
+468
+469
+470
+471
+472
+473
+474
+475
+476
+477
+478
+479
+480
+481
+482
+483
def test_cli_cwd(folder):
     """
     Test outfile cli flag.
     """
@@ -25314,6 +37626,7 @@ 

assert os.path.exists(outfile) rmpath(outfile)

+
@@ -25331,13 +37644,51 @@

+

Test creating torrent with empty files.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("version", ["1", "2", "3"])
+          
364
+365
+366
+367
+368
+369
+370
+371
+372
+373
+374
+375
+376
+377
+378
+379
+380
+381
+382
+383
+384
+385
+386
+387
+388
+389
+390
+391
+392
+393
+394
+395
+396
+397
+398
+399
+400
+401
@pytest.mark.parametrize("version", ["1", "2", "3"])
 @pytest.mark.parametrize("progress", ["0", "1"])
 def test_cli_empty_files(dir2, version, progress):
     """
@@ -25376,6 +37727,7 @@ 

assert os.path.exists(outfile) rmpath(outfile)

+
@@ -25393,13 +37745,23 @@

+

Test showing help notice cli flag.

Source code in tests\test_cli.py -
def test_cli_help():
+          
352
+353
+354
+355
+356
+357
+358
+359
+360
+361
def test_cli_help():
     """
     Test showing help notice cli flag.
     """
@@ -25410,6 +37772,7 @@ 

except SystemExit: assert True

+
@@ -25427,13 +37790,37 @@

+

Test outfile cli flag.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
+          
199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
 @pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_outfile(dir1, piece_length, version):
     """
@@ -25458,6 +37845,7 @@ 

assert os.path.exists(outfile) rmpath(outfile)

+
@@ -25475,13 +37863,36 @@

+

Test piece length cli flag.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
+          
 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
 @pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_piece_length(folder, piece_length, version):
     """
@@ -25505,6 +37916,7 @@ 

meta = pyben.load(torrent) assert meta["info"]["piece length"] == piece_length

+
@@ -25522,13 +37934,23 @@

+

Test private cli flag.

Source code in tests\test_cli.py -
def test_cli_private(folder):
+          
85
+86
+87
+88
+89
+90
+91
+92
+93
+94
def test_cli_private(folder):
     """
     Test private cli flag.
     """
@@ -25539,6 +37961,7 @@ 

meta = pyben.load(torrent) assert "private" in meta["info"]

+
@@ -25556,13 +37979,36 @@

+

Test if output when outpath ends with a /.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("sep", ["/", "\\"])
+          
426
+427
+428
+429
+430
+431
+432
+433
+434
+435
+436
+437
+438
+439
+440
+441
+442
+443
+444
+445
+446
+447
+448
@pytest.mark.parametrize("sep", ["/", "\\"])
 def test_cli_slash_outpath(dir1, sep):
     """
     Test if output when outpath ends with a /.
@@ -25586,6 +38032,7 @@ 

assert os.path.exists(outfile) rmpath(outfile)

+
@@ -25603,13 +38050,33 @@

+

Test if output when path ends with a /.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("ending", ["/", "\\"])
+          
404
+405
+406
+407
+408
+409
+410
+411
+412
+413
+414
+415
+416
+417
+418
+419
+420
+421
+422
+423
@pytest.mark.parametrize("ending", ["/", "\\"])
 def test_cli_slash_path(dir1, ending):
     """
     Test if output when path ends with a /.
@@ -25630,6 +38097,7 @@ 

assert os.path.exists(outfile) rmpath(outfile)

+
@@ -25647,13 +38115,22 @@

+

Basic create torrent cli command.

Source code in tests\test_cli.py -
def test_cli_v1(folder):
+          
52
+53
+54
+55
+56
+57
+58
+59
+60
def test_cli_v1(folder):
     """
     Basic create torrent cli command.
     """
@@ -25663,6 +38140,7 @@ 

execute() assert os.path.exists(torrent)

+
@@ -25680,13 +38158,22 @@

+

Create torrent v2 cli command.

Source code in tests\test_cli.py -
def test_cli_v2(folder):
+          
63
+64
+65
+66
+67
+68
+69
+70
+71
def test_cli_v2(folder):
     """
     Create torrent v2 cli command.
     """
@@ -25696,6 +38183,7 @@ 

execute() assert os.path.exists(torrent)

+
@@ -25713,13 +38201,22 @@

+

Create hybrid torrent cli command.

Source code in tests\test_cli.py -
def test_cli_v3(folder):
+          
74
+75
+76
+77
+78
+79
+80
+81
+82
def test_cli_v3(folder):
     """
     Create hybrid torrent cli command.
     """
@@ -25729,6 +38226,7 @@ 

execute() assert os.path.exists(torrent)

+
@@ -25746,13 +38244,37 @@

+

Test if created torrents recieve a web seeds field in meta info.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
+          
278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
 @pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_web_seeds(folder, piece_length, version):
     """
@@ -25777,6 +38299,7 @@ 

meta = pyben.load(torrent) assert "https://webseed.url/1" in meta["url-list"]

+
@@ -25794,13 +38317,35 @@

+

Test debug mode cli flag.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
+          
304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
 @pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_with_debug(folder, piece_length, version):
     """
@@ -25823,6 +38368,7 @@ 

execute() assert os.path.exists(torrent)

+
@@ -25840,13 +38386,35 @@

+

Test source cli flag.

Source code in tests\test_cli.py -
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
+          
328
+329
+330
+331
+332
+333
+334
+335
+336
+337
+338
+339
+340
+341
+342
+343
+344
+345
+346
+347
+348
+349
@pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
 @pytest.mark.parametrize("version", ["1", "2", "3"])
 def test_cli_with_source(folder, piece_length, version):
     """
@@ -25869,6 +38437,7 @@ 

meta = pyben.load(torrent) assert meta["info"]["source"] == "somesource"

+
@@ -25886,18 +38455,24 @@

+

Test dir1 fixture is not None.

Source code in tests\test_cli.py -
def test_fix():
+          
34
+35
+36
+37
+38
def test_fix():
     """
     Test dir1 fixture is not None.
     """
     assert dir1 and dir2
 
+
@@ -25907,7 +38482,6 @@

-

@@ -25952,13 +38526,37 @@

+

Test Unicode information in CLI args.

Source code in tests\test_commands.py -
def test_create_unicode_name(file1):
+          
145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
def test_create_unicode_name(file1):
     """
     Test Unicode information in CLI args.
     """
@@ -25983,6 +38581,7 @@ 

execute() assert os.path.exists(filename)

+
@@ -26000,18 +38599,24 @@

+

Test dir1 fixture is not None.

Source code in tests\test_commands.py -
def test_fix():
+          
37
+38
+39
+40
+41
def test_fix():
     """
     Test dir1 fixture is not None.
     """
     assert dir1 and metafile1 and file1 and metafile2 and dir2
 
+
@@ -26029,13 +38634,53 @@

+

Test the info_command action from the Command Line Interface.

Source code in tests\test_commands.py -
@pytest.mark.parametrize(
+          
 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
@pytest.mark.parametrize(
     "field",
     ["name", "announce", "source", "comment", "private", "announce-list"],
 )
@@ -26076,6 +38721,7 @@ 

output = info(Space) assert field in output

+
@@ -26093,19 +38739,26 @@

+

Test create magnet function scheme.

Source code in tests\test_commands.py -
def test_magnet(metafile1):
+          
65
+66
+67
+68
+69
+70
def test_magnet(metafile1):
     """
     Test create magnet function scheme.
     """
     magnet_link = magnet(metafile1)
     assert magnet_link.startswith("magnet")
 
+
@@ -26123,13 +38776,20 @@

+

Test magnet creation through CLI interface.

Source code in tests\test_commands.py -
def test_magnet_cli(metafile1):
+          
136
+137
+138
+139
+140
+141
+142
def test_magnet_cli(metafile1):
     """
     Test magnet creation through CLI interface.
     """
@@ -26137,6 +38797,7 @@ 

uri = execute() assert "magnet" in uri

+
@@ -26154,13 +38815,21 @@

+

Test create magnet function scheme.

Source code in tests\test_commands.py -
def test_magnet_empty():
+          
84
+85
+86
+87
+88
+89
+90
+91
def test_magnet_empty():
     """
     Test create magnet function scheme.
     """
@@ -26169,6 +38838,7 @@ 

except FileNotFoundError: assert True

+
@@ -26186,13 +38856,22 @@

+

Test create magnet function digest.

Source code in tests\test_commands.py -
def test_magnet_hex(metafile1):
+          
54
+55
+56
+57
+58
+59
+60
+61
+62
def test_magnet_hex(metafile1):
     """
     Test create magnet function digest.
     """
@@ -26202,6 +38881,7 @@ 

binfo = sha1(pyben.dumps(info)).hexdigest().upper() assert binfo in magnet_link

+
@@ -26219,13 +38899,22 @@

Test create magnet function scheme.

Source code in tests\test_commands.py -
def test_magnet_no_announce_list(metafile2):
+          
73
+74
+75
+76
+77
+78
+79
+80
+81
def test_magnet_no_announce_list(metafile2):
     """
     Test create magnet function scheme.
     """
@@ -26235,6 +38924,7 @@ 

magnet_link = magnet(metafile2) assert magnet_link.startswith("magnet")

+

@@ -26252,13 +38942,21 @@

+

Test create magnet function digest.

Source code in tests\test_commands.py -
def test_magnet_uri(metafile1):
+          
44
+45
+46
+47
+48
+49
+50
+51
def test_magnet_uri(metafile1):
     """
     Test create magnet function digest.
     """
@@ -26267,6 +38965,7 @@ 

announce = meta["announce"] assert quote_plus(announce) in magnet_link

+
@@ -26284,13 +38983,22 @@

+

Test running merkle root function with 1 and 0 len lists.

Source code in tests\test_commands.py -
@pytest.mark.parametrize("blocks", [[], [sha1(b"1010").digest()]])  # nosec
+          
171
+172
+173
+174
+175
+176
+177
+178
+179
@pytest.mark.parametrize("blocks", [[], [sha1(b"1010").digest()]])  # nosec
 def test_merkle_root_no_blocks(blocks):
     """
     Test running merkle root function with 1 and 0 len lists.
@@ -26300,6 +39008,7 @@ 

else: assert not merkle_root(blocks)

+
@@ -26317,13 +39026,30 @@

+

Test progbar mixins with small file.

Source code in tests\test_commands.py -
@pytest.mark.parametrize("torrent", torrents())
+          
182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
@pytest.mark.parametrize("torrent", torrents())
 def test_mixins_progbar(torrent):
     """
     Test progbar mixins with small file.
@@ -26341,6 +39067,7 @@ 

assert output == str(tfile) + ".torrent" rmpath(tfile)

+
@@ -26350,7 +39077,6 @@

-

@@ -26395,13 +39121,46 @@

+

Test edit torrent with all params on cli.

Source code in tests\test_edit.py -
@pytest.mark.parametrize("comment", ["commenta", "commentb", "commentc"])
+          
198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
@pytest.mark.parametrize("comment", ["commenta", "commentb", "commentc"])
 @pytest.mark.parametrize("source", ["sourcea", "sourceb", "sourcec"])
 @pytest.mark.parametrize("announce", [["url1", "url2", "url3"], ["url1"]])
 @pytest.mark.parametrize("webseed", [["ftp1"], ["ftpa", "ftpb"]])
@@ -26435,6 +39194,7 @@ 

assert meta["announce-list"] == [[announce]] assert meta["url-list"] == [webseed]

+
@@ -26452,13 +39212,23 @@

+

Test edit torrent with comment param.

Source code in tests\test_edit.py -
@pytest.mark.parametrize("comment", ["COMMENT", "COMIT", "MITCO"])
+          
118
+119
+120
+121
+122
+123
+124
+125
+126
+127
@pytest.mark.parametrize("comment", ["COMMENT", "COMIT", "MITCO"])
 def test_edit_comment(metafile2, comment):
     """
     Test edit torrent with comment param.
@@ -26469,6 +39239,7 @@ 

assert data == meta assert data["info"]["comment"] == comment

+
@@ -26486,13 +39257,25 @@

+

Test edit torrent with webseed param as string.

Source code in tests\test_edit.py -
@pytest.mark.parametrize(
+          
104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
@pytest.mark.parametrize(
     "httpseed", [["urla"], ["urlb", "urlc"], ["urla", "urlb", "urlc"]]
 )
 def test_edit_httpseeds(metafile2, httpseed):
@@ -26505,6 +39288,7 @@ 

assert data == meta assert data["httpseeds"] == httpseed

+
@@ -26522,13 +39306,23 @@

+

Test edit torrent with webseed param.

Source code in tests\test_edit.py -
@pytest.mark.parametrize("httpseeds", ["urla", "urlb urlc", "urla urlb urlc"])
+          
78
+79
+80
+81
+82
+83
+84
+85
+86
+87
@pytest.mark.parametrize("httpseeds", ["urla", "urlb urlc", "urla urlb urlc"])
 def test_edit_httpseeds_str(metafile2, httpseeds):
     """
     Test edit torrent with webseed param.
@@ -26539,6 +39333,7 @@ 

assert data == meta assert data["httpseeds"] == httpseeds.split()

+
@@ -26556,13 +39351,28 @@

+

Test edit torrent with None for all params.

Source code in tests\test_edit.py -
def test_edit_none(metafile2):
+          
164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
def test_edit_none(metafile2):
     """
     Test edit torrent with None for all params.
     """
@@ -26578,6 +39388,7 @@ 

meta = pyben.load(metafile2) assert data == meta == edited

+
@@ -26595,13 +39406,22 @@

+

Test edit torrent with private param False.

Source code in tests\test_edit.py -
def test_edit_private_false(metafile2):
+          
153
+154
+155
+156
+157
+158
+159
+160
+161
def test_edit_private_false(metafile2):
     """
     Test edit torrent with private param False.
     """
@@ -26611,6 +39431,7 @@ 

assert data == meta assert "private" not in data["info"]

+
@@ -26628,13 +39449,22 @@

+

Test edit torrent with private param.

Source code in tests\test_edit.py -
def test_edit_private_true(metafile2):
+          
142
+143
+144
+145
+146
+147
+148
+149
+150
def test_edit_private_true(metafile2):
     """
     Test edit torrent with private param.
     """
@@ -26644,6 +39474,7 @@ 

assert data == meta assert data["info"]["private"] == 1

+
@@ -26661,13 +39492,28 @@

+

Test edit torrent with empty for all params.

Source code in tests\test_edit.py -
def test_edit_removal(metafile2):
+          
181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
def test_edit_removal(metafile2):
     """
     Test edit torrent with empty for all params.
     """
@@ -26683,6 +39529,7 @@ 

meta = pyben.load(metafile2) assert data == meta

+
@@ -26700,13 +39547,23 @@

+

Test edit torrent with source param.

Source code in tests\test_edit.py -
@pytest.mark.parametrize("source", ["SomeSource", "NoSouce", "MidSource"])
+          
130
+131
+132
+133
+134
+135
+136
+137
+138
+139
@pytest.mark.parametrize("source", ["SomeSource", "NoSouce", "MidSource"])
 def test_edit_source(metafile2, source):
     """
     Test edit torrent with source param.
@@ -26717,6 +39574,7 @@ 

assert data == meta assert data["info"]["source"] == source

+
@@ -26734,13 +39592,25 @@

+

Test edit torrent with announce param.

Source code in tests\test_edit.py -
@pytest.mark.parametrize(
+          
40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
@pytest.mark.parametrize(
     "announce", [["urla"], ["urlb", "urlc"], ["urla", "urlb", "urlc"]]
 )
 def test_edit_torrent(metafile2, announce):
@@ -26753,6 +39623,7 @@ 

assert data == meta assert data["announce-list"] == [announce]

+
@@ -26770,13 +39641,23 @@

+

Test edit torrent with announce param as string.

Source code in tests\test_edit.py -
@pytest.mark.parametrize("announce", ["urla", "urlb urlc", "urla urlb urlc"])
+          
54
+55
+56
+57
+58
+59
+60
+61
+62
+63
@pytest.mark.parametrize("announce", ["urla", "urlb urlc", "urla urlb urlc"])
 def test_edit_torrent_str(metafile2, announce):
     """
     Test edit torrent with announce param as string.
@@ -26787,6 +39668,7 @@ 

assert data == meta assert data["announce-list"] == [announce.split()]

+
@@ -26804,13 +39686,25 @@

+

Test edit torrent with webseed param as string.

Source code in tests\test_edit.py -
@pytest.mark.parametrize(
+          
 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
@pytest.mark.parametrize(
     "url_list", [["urla"], ["urlb", "urlc"], ["urla", "urlb", "urlc"]]
 )
 def test_edit_urllist(metafile2, url_list):
@@ -26823,6 +39717,7 @@ 

assert data == meta assert data["url-list"] == url_list

+
@@ -26840,13 +39735,23 @@

+

Test edit torrent with webseed param.

Source code in tests\test_edit.py -
@pytest.mark.parametrize("url_list", ["urla", "urlb urlc", "urla urlb urlc"])
+          
66
+67
+68
+69
+70
+71
+72
+73
+74
+75
@pytest.mark.parametrize("url_list", ["urla", "urlb urlc", "urla urlb urlc"])
 def test_edit_urllist_str(metafile2, url_list):
     """
     Test edit torrent with webseed param.
@@ -26857,6 +39762,7 @@ 

assert data == meta assert data["url-list"] == url_list.split()

+
@@ -26874,18 +39780,24 @@

+

Testing dir fixtures.

Source code in tests\test_edit.py -
def test_fix():
+          
33
+34
+35
+36
+37
def test_fix():
     """
     Testing dir fixtures.
     """
     assert dir2 and metafile2 and dir1
 
+
@@ -26903,13 +39815,27 @@

+

Test if editing full unicode works as it should.

Source code in tests\test_edit.py -
def test_metafile_edit_with_unicode(metafile2):
+          
233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
def test_metafile_edit_with_unicode(metafile2):
     """
     Test if editing full unicode works as it should.
     """
@@ -26924,6 +39850,7 @@ 

msg = edits["comment"] assert com1 == com2 == msg

+
@@ -26933,7 +39860,6 @@

@@ -26979,18 +39905,24 @@

+

Test the fixtures used in module.

Source code in tests\test_interactive.py -
def test_fixtures():
+          
37
+38
+39
+40
+41
def test_fixtures():
     """
     Test the fixtures used in module.
     """
     assert filemeta2 and file1 and file2
 
+
@@ -27008,13 +39940,53 @@

+

Test creating torrent interactively with many parameters.

Source code in tests\test_interactive.py -
@pytest.mark.parametrize("version", ["1", "2", "3"])
+          
 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
@pytest.mark.parametrize("version", ["1", "2", "3"])
 @pytest.mark.parametrize("piece_length", ["23", "18", "131072"])
 @pytest.mark.parametrize("announce", ["url1", "urla urlb urlc"])
 @pytest.mark.parametrize("url_list", ["ftp url2", "ftp1 ftp2 ftp3"])
@@ -27055,6 +40027,7 @@ 

assert meta["info"]["comment"] == comment assert meta["url-list"] == url_list.split()

+
@@ -27072,13 +40045,46 @@

+

Test editing torrent interactively from CLI.

Source code in tests\test_interactive.py -
@pytest.mark.parametrize("announce", ["urla urlb urlc", "urld url2"])
+          
145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
@pytest.mark.parametrize("announce", ["urla urlb urlc", "urld url2"])
 @pytest.mark.parametrize("urllist", ["ftp url2", "ftp1 ftp2 ftp3"])
 @pytest.mark.parametrize("cmnt", ["Some Comment"])
 @pytest.mark.parametrize("srce", ["Do", "Ra"])
@@ -27112,6 +40118,7 @@ 

assert meta2["url-list"] == urllist.split() assert meta2["info"]["private"] == 1

+
@@ -27129,13 +40136,47 @@

+

Test editing torrent file interactively.

Source code in tests\test_interactive.py -
@pytest.mark.parametrize("announce", ["url1"])
+          
109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
@pytest.mark.parametrize("announce", ["url1"])
 @pytest.mark.parametrize("url_list", ["ftp url2", "ftp1 ftp2 ftp3"])
 @pytest.mark.parametrize("comment", ["Some Comment", "No Comment"])
 @pytest.mark.parametrize("source", ["Fa", "So", "La"])
@@ -27170,6 +40211,7 @@ 

assert meta1["url-list"] == url_list.split() assert meta1["info"]["private"] == 1

+
@@ -27187,13 +40229,25 @@

+

Test interactive recheck function.

Source code in tests\test_interactive.py -
@pytest.mark.parametrize("torrentclass", torrents())
+          
180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
@pytest.mark.parametrize("torrentclass", torrents())
 def test_inter_recheck(torrentclass, monkeypatch, file1):
     """
     Test interactive recheck function.
@@ -27206,6 +40260,7 @@ 

result = select_action() assert result == 100

+
@@ -27223,13 +40278,34 @@

+

Test creating torrent interactively.

Source code in tests\test_interactive.py -
def test_interactive_create(monkeypatch, file1):
+          
44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
def test_interactive_create(monkeypatch, file1):
     """
     Test creating torrent interactively.
     """
@@ -27251,6 +40327,7 @@ 

select_action() assert os.path.exists(str(file1) + ".torrent")

+
@@ -27260,7 +40337,6 @@

-

@@ -27305,13 +40381,20 @@

+

Test Checker class with directory that points to nothing.

Source code in tests\test_recheck.py -
def test_checker_callback(dir1, metafile1):
+          
125
+126
+127
+128
+129
+130
+131
def test_checker_callback(dir1, metafile1):
     """
     Test Checker class with directory that points to nothing.
     """
@@ -27319,6 +40402,7 @@ 

checker = Checker(metafile1, str(dir1)) assert checker.results() == 100

+
@@ -27336,19 +40420,26 @@

+

Test Checker Class against meta files.

Source code in tests\test_recheck.py -
def test_checker_class(dir1, metafile1):
+          
42
+43
+44
+45
+46
+47
def test_checker_class(dir1, metafile1):
     """
     Test Checker Class against meta files.
     """
     checker = Checker(metafile1, dir1)
     assert checker.results() == 100
 
+
@@ -27366,13 +40457,31 @@

+

Test Checker class when all files are missing from contents.

Source code in tests\test_recheck.py -
def test_checker_class_allfiles(sizedfiles, dir2):
+          
191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
def test_checker_class_allfiles(sizedfiles, dir2):
     """
     Test Checker class when all files are missing from contents.
     """
@@ -27391,6 +40500,7 @@ 

checker = Checker(sizedfiles, dir2) assert int(checker.results()) < 100

+
@@ -27408,13 +40518,21 @@

+

Test Checker class when all files are missing from contents.

Source code in tests\test_recheck.py -
def test_checker_class_allpaths(sizedfiles, dir2):
+          
211
+212
+213
+214
+215
+216
+217
+218
def test_checker_class_allpaths(sizedfiles, dir2):
     """
     Test Checker class when all files are missing from contents.
     """
@@ -27423,6 +40541,7 @@ 

checker = Checker(sizedfiles, dir2) assert int(checker.results()) < 100

+
@@ -27440,13 +40559,25 @@

+

Test Checker class with half size single file.

Source code in tests\test_recheck.py -
def test_checker_class_half_file(filemeta2, file2):
+          
221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
def test_checker_class_half_file(filemeta2, file2):
     """
     Test Checker class with half size single file.
     """
@@ -27459,6 +40590,7 @@ 

checker = Checker(filemeta2, file2) assert int(checker.results()) != 10

+
@@ -27476,13 +40608,20 @@

+

Test exclusive Checker Mode CLI.

Source code in tests\test_recheck.py -
def test_checker_cli_args(dir1, metafile1):
+          
134
+135
+136
+137
+138
+139
+140
def test_checker_cli_args(dir1, metafile1):
     """
     Test exclusive Checker Mode CLI.
     """
@@ -27490,6 +40629,7 @@ 

output = main() assert output == 100

+
@@ -27507,13 +40647,34 @@

+

Test Checker when directory contains 0 length files.

Source code in tests\test_recheck.py -
def test_checker_empty_files(dir2, sizedfiles):
+          
274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
def test_checker_empty_files(dir2, sizedfiles):
     """
     Test Checker when directory contains 0 length files.
     """
@@ -27535,6 +40696,7 @@ 

checker = Checker(sizedfiles, dir2) assert checker.results() != 100

+
@@ -27552,13 +40714,37 @@

+

Test Checker Class when first piece is slightly alterred.

Source code in tests\test_recheck.py -
def test_checker_first_piece(dir2, sizedfiles):
+          
50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
def test_checker_first_piece(dir2, sizedfiles):
     """
     Test Checker Class when first piece is slightly alterred.
     """
@@ -27583,6 +40769,7 @@ 

checker = Checker(sizedfiles, dir2) assert checker.results() != 100

+
@@ -27600,13 +40787,36 @@

+

Test Checker Class when first piece is slightly alterred.

Source code in tests\test_recheck.py -
def test_checker_first_piece_alt(dir2, sizedfiles):
+          
76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
def test_checker_first_piece_alt(dir2, sizedfiles):
     """
     Test Checker Class when first piece is slightly alterred.
     """
@@ -27630,6 +40840,7 @@ 

checker = Checker(sizedfiles, dir2) assert checker.results() != 100

+
@@ -27647,13 +40858,23 @@

+

Test Checker class when files are missing from contents.

Source code in tests\test_recheck.py -
def test_checker_missing(sizedfiles, dir2):
+          
179
+180
+181
+182
+183
+184
+185
+186
+187
+188
def test_checker_missing(sizedfiles, dir2):
     """
     Test Checker class when files are missing from contents.
     """
@@ -27664,6 +40885,7 @@ 

checker = Checker(sizedfiles, dir2) assert int(checker.results()) < 100

+
@@ -27681,13 +40903,33 @@

+

Test Checker class with half size single file.

Source code in tests\test_recheck.py -
def test_checker_missing_singles(dir2, sizedfiles):
+          
235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
def test_checker_missing_singles(dir2, sizedfiles):
     """
     Test Checker class with half size single file.
     """
@@ -27708,6 +40950,7 @@ 

checker = Checker(sizedfiles, dir2) assert int(checker.results()) < 100

+
@@ -27725,13 +40968,21 @@

+

Test Checker when incorrect metafile is provided.

Source code in tests\test_recheck.py -
def test_checker_no_meta_file():
+          
159
+160
+161
+162
+163
+164
+165
+166
def test_checker_no_meta_file():
     """
     Test Checker when incorrect metafile is provided.
     """
@@ -27740,6 +40991,7 @@ 

except FileNotFoundError: assert True

+
@@ -27757,19 +41009,26 @@

+

Test providing the parent directory for torrent checking feature.

Source code in tests\test_recheck.py -
def test_checker_parent_dir(dir1, metafile1):
+          
143
+144
+145
+146
+147
+148
def test_checker_parent_dir(dir1, metafile1):
     """
     Test providing the parent directory for torrent checking feature.
     """
     checker = Checker(metafile1, os.path.dirname(dir1))
     assert checker.results() == 100
 
+
@@ -27787,13 +41046,20 @@

+

Test Checker class with half size single file.

Source code in tests\test_recheck.py -
def test_checker_result_property(dir1, metafile1):
+          
257
+258
+259
+260
+261
+262
+263
def test_checker_result_property(dir1, metafile1):
     """
     Test Checker class with half size single file.
     """
@@ -27801,6 +41067,7 @@ 

result = checker.results() assert checker.results() == result

+
@@ -27818,19 +41085,26 @@

+

Test the simplest example.

Source code in tests\test_recheck.py -
def test_checker_simplest(dir1, metafile1):
+          
266
+267
+268
+269
+270
+271
def test_checker_simplest(dir1, metafile1):
     """
     Test the simplest example.
     """
     checker = Checker(metafile1, dir1)
     assert checker.results() == 100
 
+
@@ -27848,19 +41122,26 @@

+

Test checker with single file torrent.

Source code in tests\test_recheck.py -
def test_checker_with_file(file1, filemeta1):
+          
151
+152
+153
+154
+155
+156
def test_checker_with_file(file1, filemeta1):
     """
     Test checker with single file torrent.
     """
     checker = Checker(filemeta1, file1)
     assert checker.results() == 100
 
+
@@ -27878,13 +41159,21 @@

+

Test Checker when incorrect root directory is provided.

Source code in tests\test_recheck.py -
def test_checker_wrong_root_dir(metafile1):
+          
169
+170
+171
+172
+173
+174
+175
+176
def test_checker_wrong_root_dir(metafile1):
     """
     Test Checker when incorrect root directory is provided.
     """
@@ -27893,6 +41182,7 @@ 

except FileNotFoundError: assert True

+
@@ -27910,13 +41200,20 @@

+

Test fixtures exist.

Source code in tests\test_recheck.py -
def test_fixtures():
+          
33
+34
+35
+36
+37
+38
+39
def test_fixtures():
     """
     Test fixtures exist.
     """
@@ -27924,6 +41221,7 @@ 

assert filemeta1 and filemeta2 and metafile1 assert metafile2 and sizes and sizedfiles

+
@@ -27941,13 +41239,35 @@

+

Test Checker with data that is expected to be incomplete.

Source code in tests\test_recheck.py -
def test_partial_metafiles(dir2, sizedfiles):
+          
101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
def test_partial_metafiles(dir2, sizedfiles):
     """
     Test Checker with data that is expected to be incomplete.
     """
@@ -27970,6 +41290,7 @@ 

checker = Checker(sizedfiles, testdir) assert checker.results() != 100

+
@@ -27987,13 +41308,22 @@

+

Test recheck function with directory that doesn't contain the contents.

Source code in tests\test_recheck.py -
def test_recheck_wrong_dir(metafile1):
+          
297
+298
+299
+300
+301
+302
+303
+304
+305
def test_recheck_wrong_dir(metafile1):
     """
     Test recheck function with directory that doesn't contain the contents.
     """
@@ -28003,6 +41333,7 @@ 

except FileNotFoundError: assert True

+
@@ -28012,7 +41343,6 @@

-

@@ -28057,13 +41387,34 @@

+

Test cwd argument with create command failure.

Source code in tests\test_torrent.py -
def test_create_cwd_fail():
+          
175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
def test_create_cwd_fail():
     """Test cwd argument with create command failure."""
 
     class SuFile:
@@ -28085,6 +41436,7 @@ 

assert os.path.exists(current) rmpath(tfile, current)

+
@@ -28102,18 +41454,24 @@

+

Test pytest fixtures.

Source code in tests\test_torrent.py -
def test_fixtures():
+          
32
+33
+34
+35
+36
def test_fixtures():
     """
     Test pytest fixtures.
     """
     assert dir1 and dir2
 
+
@@ -28131,13 +41489,29 @@

+

Test torrent creation for file size larger than 10MB.

Source code in tests\test_torrent.py -
@pytest.mark.parametrize("version", torrents())
+          
209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
@pytest.mark.parametrize("version", torrents())
 @pytest.mark.parametrize("progress", [0, 1, 2])
 def test_mbtorrent(version, progress):
     """
@@ -28154,6 +41528,7 @@ 

assert os.path.exists(outfile) rmpath(tfile, outfile)

+
@@ -28171,13 +41546,22 @@

+

Test assembling base metafile exception.

Source code in tests\test_torrent.py -
def test_metafile_assemble(dir1):
+          
50
+51
+52
+53
+54
+55
+56
+57
+58
def test_metafile_assemble(dir1):
     """
     Test assembling base metafile exception.
     """
@@ -28187,6 +41571,7 @@ 

except NotImplementedError: assert True

+
@@ -28204,13 +41589,38 @@

+

Test creating a torrent meta file with given directory plus extra.

Source code in tests\test_torrent.py -
@pytest.mark.parametrize("version", torrents())
+          
 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
@pytest.mark.parametrize("version", torrents())
 def test_torrentfile_extra(dir2, version):
     """
     Test creating a torrent meta file with given directory plus extra.
@@ -28236,6 +41646,7 @@ 

torrent = version(**args) assert torrent.meta["announce"] == "announce"

+
@@ -28253,13 +41664,22 @@

Test missing path error exception.

Source code in tests\test_torrent.py -
@pytest.mark.parametrize("version", torrents())
+          
39
+40
+41
+42
+43
+44
+45
+46
+47
@pytest.mark.parametrize("version", torrents())
 def test_torrentfile_missing_path(version):
     """
     Test missing path error exception.
@@ -28269,6 +41689,7 @@ 

except MissingPathError: assert True

+
@@ -28286,13 +41707,29 @@

+

Test creating a torrent meta file with given directory plus extra.

Source code in tests\test_torrent.py -
@pytest.mark.parametrize("version", torrents())
+          
61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
@pytest.mark.parametrize("version", torrents())
 def test_torrentfile_one_empty(dir2, version):
     """
     Test creating a torrent meta file with given directory plus extra.
@@ -28309,6 +41746,7 @@ 

torrent = version(**args) assert torrent.meta["announce"] == "announce"

+
@@ -28326,13 +41764,33 @@

+

Test creating a torrent file from a single file contents.

Source code in tests\test_torrent.py -
@pytest.mark.parametrize("num", list(range(17, 25)))
+          
106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
@pytest.mark.parametrize("num", list(range(17, 25)))
 @pytest.mark.parametrize("piece_length", [2**i for i in range(14, 18)])
 @pytest.mark.parametrize("version", torrents())
 def test_torrentfile_single(version, num, piece_length, capsys):
@@ -28353,6 +41811,7 @@ 

assert os.path.exists(str(tfile) + ".torrent") rmpath(tfile, str(tfile) + ".torrent")

+
@@ -28370,13 +41829,34 @@

Test creating a torrent file from a single file contents plus extra.

Source code in tests\test_torrent.py -
@pytest.mark.parametrize("size", list(range(17, 25)))
+          
128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
@pytest.mark.parametrize("size", list(range(17, 25)))
 @pytest.mark.parametrize("piece_length", [2**i for i in range(14, 18)])
 @pytest.mark.parametrize("version", torrents())
 def test_torrentfile_single_extra(version, size, piece_length):
@@ -28398,6 +41878,7 @@ 

assert os.path.exists(outfile) rmpath(tfile, outfile)

+
@@ -28415,13 +41896,35 @@

Test creating a torrent file from less than a single file contents.

Source code in tests\test_torrent.py -
@pytest.mark.parametrize("sze", list(range(17, 25)))
+          
151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
@pytest.mark.parametrize("sze", list(range(17, 25)))
 @pytest.mark.parametrize("piecelength", [2**i for i in range(14, 18)])
 @pytest.mark.parametrize("ver", torrents())
 def test_torrentfile_single_under(ver, sze, piecelength):
@@ -28444,6 +41947,7 @@ 

assert os.path.exists(outfile) rmpath(tfile, outfile)

+
@@ -28461,13 +41965,22 @@

+

Test waiting function.

Source code in tests\test_torrent.py -
def test_waiting_mixin():
+          
198
+199
+200
+201
+202
+203
+204
+205
+206
def test_waiting_mixin():
     """
     Test waiting function.
     """
@@ -28477,6 +41990,7 @@ 

waiting(msg, lst, timeout=timeout) assert len(lst) == 0

+
@@ -28486,7 +42000,6 @@

- @@ -28531,19 +42044,26 @@

+

Test function for acquiring a filelist for directory.

Source code in tests\test_utils.py -
def test_filelist_total(dir1):
+          
118
+119
+120
+121
+122
+123
def test_filelist_total(dir1):
     """
     Test function for acquiring a filelist for directory.
     """
     total, _ = utils.filelist_total(dir1)
     assert total == (2**18) * 8
 
+
@@ -28561,32 +42081,50 @@

+

Test function filelist total with missing path.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
dir2pytest.fixture

fixture containing a temporary directory

requiredNameTypeDescriptionDefault
+ + + + dir2 + + pytest.fixture + +

fixture containing a temporary directory

+ + required + + + + +
Source code in tests\test_utils.py -
def test_filelisttotal_missing(dir2):
+          
225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
def test_filelisttotal_missing(dir2):
     """Test function filelist total with missing path.
 
     Parameters
@@ -28600,6 +42138,7 @@ 

except utils.MissingPathError: assert True

+
@@ -28617,19 +42156,26 @@

+

Test function for get a list of files in a directory.

Source code in tests\test_utils.py -
def test_get_filelist(dir1):
+          
102
+103
+104
+105
+106
+107
def test_get_filelist(dir1):
     """
     Test function for get a list of files in a directory.
     """
     filelist = utils.get_file_list(dir1)
     assert len(filelist) == 8
 
+
@@ -28647,18 +42193,24 @@

+

Test function for getting piece length for folders max.

Source code in tests\test_utils.py -
def test_get_path_length_max(dir1):
+          
71
+72
+73
+74
+75
def test_get_path_length_max(dir1):
     """
     Test function for getting piece length for folders max.
     """
     assert utils.path_piece_length(dir1) <= (2**27)
 
+
@@ -28676,18 +42228,24 @@

+

Test function for getting piece length for folders min.

Source code in tests\test_utils.py -
def test_get_path_length_min(dir1):
+          
64
+65
+66
+67
+68
def test_get_path_length_min(dir1):
     """
     Test function for getting piece length for folders min.
     """
     assert utils.path_piece_length(dir1) >= (2**14)
 
+
@@ -28705,18 +42263,24 @@

+

Test function for the best piece length for provided path.

Source code in tests\test_utils.py -
def test_get_path_length_mod(dir1):
+          
57
+58
+59
+60
+61
def test_get_path_length_mod(dir1):
     """
     Test function for the best piece length for provided path.
     """
     assert utils.path_piece_length(dir1) % (2**14) == 0
 
+
@@ -28734,19 +42298,26 @@

+

Test function for getting total size of directory.

Source code in tests\test_utils.py -
def test_get_path_size(dir1):
+          
110
+111
+112
+113
+114
+115
def test_get_path_size(dir1):
     """
     Test function for getting total size of directory.
     """
     pathsize = utils.path_size(dir1)
     assert pathsize == (2**18) * 8
 
+
@@ -28764,13 +42335,20 @@

+

Test function for best piece length for given size.

Source code in tests\test_utils.py -
@pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945])
+          
30
+31
+32
+33
+34
+35
+36
@pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945])
 def test_get_piece_length(size):
     """
     Test function for best piece length for given size.
@@ -28778,6 +42356,7 @@ 

value = utils.get_piece_length(size) assert value % 1024 == 0

+
@@ -28795,13 +42374,20 @@

+

Test function for best piece length for given size maximum.

Source code in tests\test_utils.py -
@pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945])
+          
39
+40
+41
+42
+43
+44
+45
@pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945])
 def test_get_piece_length_max(size):
     """
     Test function for best piece length for given size maximum.
@@ -28809,6 +42395,7 @@ 

value = utils.get_piece_length(size) assert value < 2**27

+
@@ -28826,13 +42413,20 @@

+

Test function for best piece length for given size minimum.

Source code in tests\test_utils.py -
@pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945])
+          
48
+49
+50
+51
+52
+53
+54
@pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945])
 def test_get_piece_length_min(size):
     """
     Test function for best piece length for given size minimum.
@@ -28840,6 +42434,7 @@ 

value = utils.get_piece_length(size) assert value >= 2**14

+
@@ -28857,13 +42452,29 @@

+

Test humanize bytes function.

Source code in tests\test_utils.py -
@pytest.mark.parametrize(
+          
160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
@pytest.mark.parametrize(
     "amount, result",
     [
         (100, "100"),
@@ -28880,35 +42491,7 @@ 

""" assert utils.humanize_bytes(amount) == result

- - - - - - - -
- - - -

-test_main() - - -

- -
- -

Test main module in torrentfile package.

- -
- Source code in tests\test_utils.py -
def test_main():
-    """
-    Test main module in torrentfile package.
-    """
-    assert vars(main)
-
+
@@ -28926,13 +42509,22 @@

+

Test exception for missing path parameter.

Source code in tests\test_utils.py -
def test_missing_path_error():
+          
137
+138
+139
+140
+141
+142
+143
+144
+145
def test_missing_path_error():
     """
     Test exception for missing path parameter.
     """
@@ -28942,6 +42534,7 @@ 

assert True assert dir2

+
@@ -28959,13 +42552,23 @@

+

Test next power of 2 function in utils module.

Source code in tests\test_utils.py -
@pytest.mark.parametrize("value", [5, 32, 18, 225, 16384, 256000])
+          
148
+149
+150
+151
+152
+153
+154
+155
+156
+157
@pytest.mark.parametrize("value", [5, 32, 18, 225, 16384, 256000])
 def test_next_power_2(value):
     """
     Test next power of 2 function in utils module.
@@ -28976,6 +42579,7 @@ 

assert result % 2 == 0 assert result >= value

+
@@ -28993,32 +42597,52 @@

+

Test function to normalize piece length errors.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - -
amountany

arguments intended to raise an exception.

requiredNameTypeDescriptionDefault
+ + + + amount + + any + +

arguments intended to raise an exception.

+ + required + + + + +
Source code in tests\test_utils.py -
@pytest.mark.parametrize(
+          
208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
@pytest.mark.parametrize(
     "amount", ["hello", 11, 0, 100000, 28, "zero", "fifteen"]
 )
 def test_norm_plength_errors(amount):
@@ -29034,6 +42658,7 @@ 

except utils.PieceLengthValueError: assert True

+
@@ -29051,38 +42676,58 @@

Test normalize piece length function.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - - - - - - - -
amount`str` or `int`

piece length or representation

requiredNameTypeDescriptionDefault
resultint

expected output.

required
+ + + + amount + + +

piece length or representation

+ + required + + + + result + + int + +

expected output.

+ + required + + + + +
Source code in tests\test_utils.py -
@pytest.mark.parametrize("amount, result", [(i, 2**i) for i in range(14, 25)])
+          
178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
@pytest.mark.parametrize("amount, result", [(i, 2**i) for i in range(14, 25)])
 def test_normalize_piece_length_int(amount, result):
     """Test normalize piece length function.
 
@@ -29095,6 +42740,7 @@ 

""" assert utils.normalize_piece_length(amount) == result

+
@@ -29112,38 +42758,60 @@

Test normalize piece length function.

-

Parameters:

- - - - - - - - - - +

Parameters:

+
NameTypeDescriptionDefault
+ - - - - + + + + - - - - - - - -
amount`str` or `int`

piece length or representation

requiredNameTypeDescriptionDefault
resultint

expected output.

required
+ + + + amount + + +

piece length or representation

+ + required + + + + result + + int + +

expected output.

+ + required + + + + +
Source code in tests\test_utils.py -
@pytest.mark.parametrize(
+          
192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
@pytest.mark.parametrize(
     "amount, result", [(str(i), 2**i) for i in range(14, 21)]
 )
 def test_normalize_piece_length_str(amount, result):
@@ -29158,6 +42826,7 @@ 

""" assert utils.normalize_piece_length(amount) == result

+
@@ -29175,19 +42844,26 @@

+

Test function for acquiring piece length information on folder.

Source code in tests\test_utils.py -
def test_path_stat(dir1):
+          
78
+79
+80
+81
+82
+83
def test_path_stat(dir1):
     """
     Test function for acquiring piece length information on folder.
     """
     _, _, piece_length = utils.path_stat(dir1)
     assert piece_length % (2**14) == 0
 
+
@@ -29205,19 +42881,26 @@

+

Test function for acquiring file list information on folder.

Source code in tests\test_utils.py -
def test_path_stat_filelist_size(dir1):
+          
94
+95
+96
+97
+98
+99
def test_path_stat_filelist_size(dir1):
     """
     Test function for acquiring file list information on folder.
     """
     filelist, _, _ = utils.path_stat(dir1)
     assert len(filelist) == 8
 
+
@@ -29235,19 +42918,26 @@

+

Test function for acquiring total size information on folder.

Source code in tests\test_utils.py -
def test_path_stat_size(dir1):
+          
86
+87
+88
+89
+90
+91
def test_path_stat_size(dir1):
     """
     Test function for acquiring total size information on folder.
     """
     _, totalsize, _ = utils.path_stat(dir1)
     assert totalsize == (2**18) * 8
 
+
@@ -29265,13 +42955,22 @@

Test exception for uninterpretable piece length value.

Source code in tests\test_utils.py -
def test_piecelength_error_fixtures():
+          
126
+127
+128
+129
+130
+131
+132
+133
+134
def test_piecelength_error_fixtures():
     """
     Test exception for uninterpretable piece length value.
     """
@@ -29281,6 +42980,7 @@ 

assert True assert dir1

+
@@ -29290,7 +42990,6 @@

- + diff --git a/site/Commands.md b/site/Commands.md index c05b3281..6feccfd3 100644 --- a/site/Commands.md +++ b/site/Commands.md @@ -57,8 +57,8 @@ -c , --comment Include a comment in file metadata -o , --out Output path for created .torrent file -t [ ...], --tracker [ ...] One or more Bittorrent tracker announce url(s). - --noprogress Disable showing the progress bar during torrent creation. - (Minimially improves performance of torrent file creation.) + --prog, --progress (0) = no progress bar displayed + (1) = progress bar is displayed (default) --meta-version Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) diff --git a/site/changelog.md b/site/changelog.md index 480aaf99..cc0cbbe1 100644 --- a/site/changelog.md +++ b/site/changelog.md @@ -1,5 +1,19 @@ # TorrentFile +## Version 0.7.9 + +- complete rewrite of the recheck procedures +- Recheck now provides more accuracy and more details +- improvements to the new custom progressbar +- changed the cli argument for the progress bar +- the options are now just 0 and 1 +- included new unit tests for all new features +- marked unused functions as deprecated +- added a new hasher object for v2 and hybrid torrents +- minor bug fixes and styling changes + +--------------------- + ## Version 0.7.8 - more updates to logging diff --git a/site/index.md b/site/index.md index 7a173a1a..8b369b12 100644 --- a/site/index.md +++ b/site/index.md @@ -78,10 +78,12 @@ Commands Distributed under Apache v2 software license. See `LICENSE` for more information. -## 💡 Issues & Requests +## 💡 Issues & Requests & PRs If you encounter any bugs or would like to request a new feature please open a new issue. +> PRs and other contributions are welcome + [https://github.com/alexpdev/torrentfile/issues](https://github.com/alexpdev/torrentfile/issues) ------ @@ -113,11 +115,10 @@ If you encounter any bugs or would like to request a new feature please open a n > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 ``` -The 3 options for controlling the progress bar using `--prog` or `--progress`: +The options for controlling the progress bar using `--prog` or `--progress`: - 0 : show no progress bar at all -- 1 : show 1 progress bar measuring the entire creation process -- 2 : show a progress bar for each file of the torrent content +- 1 : show progress bar (default) ```bash > torrentfile -t http://tracker.com --progress 2 /path/to/content diff --git a/site/man.md b/site/man.md index f0461ea2..2e5bf2f5 100644 --- a/site/man.md +++ b/site/man.md @@ -4,13 +4,13 @@ ## Synopsis - torrentfile [options] [optional] + torrentfile [options] [options] ## Description -torrentfile is a tool for creating and/or manipulating Bittorrent files(.torrent). -It also provides tools for reviewing torrent file information, generating magnet links, -and checking/validating torrent content against its companion torrent file. +torrentfile is a CLI tool for creating, editing, validating, or reviewing Bittorrent files(.torrent). +It supports all current versions of the Bittorrent Protocol files as well as hybrid files. +Has support for generating magnet URI's for meta files. ### Options @@ -21,7 +21,7 @@ displays all relevant command line options and subcommands. displays program and version. - `-v` -enables verbose mode as well as a logfile. +enables debug mode and outputs a large amount of information to the terminal. - `-i` activates interactive mode for selecting subcommands and options. @@ -30,7 +30,7 @@ activates interactive mode for selecting subcommands and options. #### create -alias: `c` +alias: `c`, `new` torrentfile create [options] torrentfile c [options] @@ -76,8 +76,8 @@ Example: if content is at `/home/user/torrents/content` the default would create Changes the default save location to the current working directory. Example: if content is at `/home/user/torrents/content` this option would create `./content.torrent` -- `--noprogress` -Turns off the progress bar indicator shown. +- `--progress` `--prog` +Options (0, 1): No status bar will be shown if 0. Otherwise the default and 1 argument means progress bar is shown #### info @@ -123,7 +123,7 @@ Indicates that the torrent will be used on a private tracker. Disables multi-tr #### recheck -alias: `r` +alias: `r`, `check` torrentfile recheck torrentfile r diff --git a/tests/test_utils.py b/tests/test_utils.py index dc21ae63..ae3fdba6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -24,17 +24,9 @@ import pytest from tests import dir1, dir2, rmpath -from torrentfile import __main__ as main from torrentfile import utils -def test_main(): - """ - Test main module in torrentfile package. - """ - assert vars(main) - - @pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945]) def test_get_piece_length(size): """ diff --git a/torrentfile/__main__.py b/torrentfile/__main__.py index b5a94cbe..1980b3c7 100644 --- a/torrentfile/__main__.py +++ b/torrentfile/__main__.py @@ -22,5 +22,13 @@ from torrentfile.cli import execute + +def main(): + """ + Start the entry point script. + """ + execute() + + if __name__ == "__main__": execute() # pragma: nocover diff --git a/torrentfile/cli.py b/torrentfile/cli.py index 6a7c1fed..e0e68b3b 100644 --- a/torrentfile/cli.py +++ b/torrentfile/cli.py @@ -43,27 +43,27 @@ def activate_logger(): """ logging.basicConfig(level=logging.INFO) logger = logging.getLogger() - file_handler = logging.FileHandler( - "torrentfile.log", mode="a+", encoding="utf-8" - ) + # file_handler = logging.FileHandler( + # "torrentfile.log", mode="a+", encoding="utf-8" + # ) console_handler = logging.StreamHandler(stream=sys.stderr) - file_formatter = logging.Formatter( - "%(asctime)s %(levelno)s %(message)s", - datefmt="%m-%d %H:%M:%S", - style="%", - ) + # file_formatter = logging.Formatter( + # "%(asctime)s %(levelno)s %(message)s", + # datefmt="%m-%d %H:%M:%S", + # style="%", + # ) stream_formatter = logging.Formatter( "%(asctime)s %(levelno)s %(message)s", datefmt="%m-%d %H:%M:%S", style="%", ) - file_handler.setFormatter(file_formatter) + # file_handler.setFormatter(file_formatter) console_handler.setFormatter(stream_formatter) - file_handler.setLevel(logging.INFO) - console_handler.setLevel(logging.INFO) - logger.setLevel(logging.INFO) + # file_handler.setLevel(logging.INFO) + console_handler.setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) logger.addHandler(console_handler) - logger.addHandler(file_handler) + # logger.addHandler(file_handler) logger.debug("Debug: ON") @@ -165,7 +165,7 @@ def format_headers(parts): return parts -def execute(args=None): +def execute(args=None) -> list: """ Initialize Command Line Interface for torrentfile. @@ -173,6 +173,11 @@ def execute(args=None): ---------- args : list Commandline arguments. default=None + + Returns + ------- + list + Depends on what the command line args were. """ if not args: if sys.argv[1:]: @@ -510,7 +515,9 @@ def execute(args=None): if args.interactive: return select_action() - return args.func(args) + if hasattr(args, "func"): + return args.func(args) + return args main_script = execute diff --git a/torrentfile/hasher.py b/torrentfile/hasher.py index ee8993c6..4787ec09 100644 --- a/torrentfile/hasher.py +++ b/torrentfile/hasher.py @@ -464,10 +464,9 @@ def _pad_remaining(self, block_count: int): if not self.layer_hashes: power2 = next_power_2(block_count) remaining = power2 - block_count - self.prog_update(HASH_SIZE * remaining) return [bytes(HASH_SIZE) for _ in range(remaining)] - def __next__(self): + def __next__(self) -> bytes: """ Calculate layer hashes for contents of file. @@ -475,6 +474,11 @@ def __next__(self): ---------- data : BytesIO File opened in read mode. + + Returns + ------- + bytes + The layer merckle root hash. """ if self.end: self.end = False @@ -486,7 +490,6 @@ def __next__(self): block = bytearray(BLOCK_SIZE) for _ in range(self.amount): size = self.current.readinto(block) - self.prog_update(size) if not size: self.end = True break @@ -498,6 +501,7 @@ def __next__(self): if len(blocks) != self.amount: padding = self._pad_remaining(len(blocks)) blocks.extend(padding) + self.prog_update(total) layer_hash = merkle_root(blocks) self.layer_hashes.append(layer_hash) if self._cb: diff --git a/torrentfile/recheck.py b/torrentfile/recheck.py index fd66391a..24fba6bf 100644 --- a/torrentfile/recheck.py +++ b/torrentfile/recheck.py @@ -534,6 +534,13 @@ class Padder: def __init__(self, length, piece_length): """ Construct padding class to Mock missing or incomplete files. + + Parameters + ---------- + length : int + size of the file + piece_length : int + the piece length for each iteration. """ self.length = length self.piece_length = piece_length @@ -545,9 +552,18 @@ def __iter__(self): """ return self # pragma: nocover - def __next__(self): + def __next__(self) -> bytes: """ Iterate through seemingly endless sha256 hashes of zeros. + + Returns + ------- + tuple : + returns the padding + + Raises + ------ + StopIteration """ if self.length >= self.piece_length: self.length -= self.piece_length @@ -558,9 +574,14 @@ def __next__(self): return pad raise StopIteration - def next_file(self): + def next_file(self) -> bool: """ Remove all references to processed files and prepare for the next. + + Returns + ------- + bool + if there is a next file found """ self.index += 1 self.prog_close() @@ -587,9 +608,18 @@ def next_file(self): del self.pieces return False - def process_current(self): + def process_current(self) -> tuple: """ Gather necessary information to compare to metafile details. + + Returns + ------- + tuple + a tuple containing the layer, piece, current path and size + + Raises + ------ + StopIteration """ try: layer = next(self.hasher) @@ -605,9 +635,14 @@ def process_current(self): return layer, piece, self.current, size raise StopIteration from err - def advance(self): + def advance(self) -> tuple: """ Increment the number of pieces processed for the current file. + + Returns + ------- + tuple + the piece and size """ start = self.count * SHA256 end = start + SHA256 diff --git a/tox.ini b/tox.ini index 21d1f9cd..b69f3542 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,7 @@ commands = pydocstyle torrentfile tests pycodestyle torrentfile tests pylint torrentfile tests - bandit torrentfile tests + bandit -r -c pyproject.toml torrentfile tests pyroma . flake8 torrentfile tests From 21807923b385d2d437d4bcea550b1c942d42a1ea Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 May 2022 20:27:01 -0700 Subject: [PATCH 2/7] supplemental improvements on documentation --- docs/search/search_index.json | 2 +- docs/sitemap.xml.gz | Bin 256 -> 256 bytes docs/source/index.html | 654 +++++++++++++--------------------- tests/__init__.py | 2 +- torrentfile/commands.py | 4 +- torrentfile/hasher.py | 5 - torrentfile/interactive.py | 2 +- torrentfile/recheck.py | 6 +- torrentfile/torrent.py | 12 +- 9 files changed, 267 insertions(+), 420 deletions(-) diff --git a/docs/search/search_index.json b/docs/search/search_index.json index 1e3ed414..b7766605 100644 --- a/docs/search/search_index.json +++ b/docs/search/search_index.json @@ -1 +1 @@ -{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"TorrentFile \ud83c\udf10 Overview A simple and convenient tool for creating, reviewing, editing, and/or validating bittorrent meta files (aka torrent files). torrentfile supports all versions of Bittorrent files, including hybrid meta files, and has full unicode support. A GUI frontend for this project can be found at https://github.com/alexpdev/TorrentfileQt \ud83d\udd0c Requirements Python 3.7+ Tested on Linux, Windows and Mac \ud83d\udcbb Install via PyPi: pip install torrentfile via Git: git clone https://github.com/alexpdev/torrentfile.git cd torrentfile pip install . Download pre-compiled binaries from the release page . \ud83d\udcda Documentation Documentation can be found here or in the docs directory. torrentfile is under active development, and is subject to significant changes in the codebase between releases. \ud83d\ude80 Usage Usage ===== torrentfile [options] [command-options] ... Create, edit, show details, and check any version Bittorrent file from command line Commands -------- create (c, new) Create a torrent meta file. edit (e) Edit existing torrent meta file. magnet (m) Create magnet url from an existing Bittorrent meta file. recheck (r, check) Calculate amount of torrent meta files content is found on disk. info (i) Show detailed information about a torrent file. Usage examples can be found in the project documentation on the examples page. \ud83d\udcdd License Distributed under Apache v2 software license. See LICENSE for more information. \ud83d\udca1 Issues & Requests & PRs If you encounter any bugs or would like to request a new feature please open a new issue. PRs and other contributions are welcome https://github.com/alexpdev/torrentfile/issues Creating Torrents Basic torrent file createion > torrentfile create /path/to/content The -t --tracker -a --announce flags add one or more urls to list of trackers. > torrentfile create /path/to/content --tracker http://tracker1.com > torrentfile create -t http://tracker2 http://tracker3 --private /path/to/content > torrentfile create --tracker http://tracker /path/to/content > torrentfile create -t http://tracker1 http://tracker2 /path/to/content the --private flag indicates use by a private tracker the --source flag can be used to help with cross-seeding > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 The options for controlling the progress bar using --prog or --progress : 0 : show no progress bar at all 1 : show progress bar (default) > torrentfile -t http://tracker.com --progress 2 /path/to/content > torrentfile --prog 0 /path/to/content to specify the save location use the -o or --out flags if the path points to directory the name of torrent is autofilled. > torrentfile create -o /specific/path/name.torrent ./content to create files using bittorrent v2 use --meta-version 2 likewise --meta-version 3 creates a hybrid torrent file. > torrentfile create --meta-version 2 /path/to/content > torrentfile create --meta-version 3 /path/to/content Check/Recheck Torrent recheck torrent file /path/to/some.torrent with /path/to/content > torrentfile recheck /path/to/some.torrent /path/to/content Edit Torrent edit a torrent file > torrentfile edit [options] Create Magnet To create a magnet URI for a pre-existing torrent meta file, use the sub-command magnet or m with the path to the torrent file. > torrentfile magnet /path/to/some.torrent Interactive Mode (expiremental) Alternatively to supplying a bunch of command line arguments, interactive mode allows users to specify program options one at a time from a series of prompts. to activate interactive mode use -i or --interactive flag > torrentfile -i GUI If you prefer a windowed gui please check out the official GUI frontend here","title":"Home"},{"location":"#torrentfile","text":"","title":"TorrentFile"},{"location":"#overview","text":"A simple and convenient tool for creating, reviewing, editing, and/or validating bittorrent meta files (aka torrent files). torrentfile supports all versions of Bittorrent files, including hybrid meta files, and has full unicode support. A GUI frontend for this project can be found at https://github.com/alexpdev/TorrentfileQt","title":"\ud83c\udf10 Overview"},{"location":"#requirements","text":"Python 3.7+ Tested on Linux, Windows and Mac","title":"\ud83d\udd0c Requirements"},{"location":"#install","text":"via PyPi: pip install torrentfile via Git: git clone https://github.com/alexpdev/torrentfile.git cd torrentfile pip install . Download pre-compiled binaries from the release page .","title":"\ud83d\udcbb Install"},{"location":"#documentation","text":"Documentation can be found here or in the docs directory. torrentfile is under active development, and is subject to significant changes in the codebase between releases.","title":"\ud83d\udcda Documentation"},{"location":"#usage","text":"Usage ===== torrentfile [options] [command-options] ... Create, edit, show details, and check any version Bittorrent file from command line Commands -------- create (c, new) Create a torrent meta file. edit (e) Edit existing torrent meta file. magnet (m) Create magnet url from an existing Bittorrent meta file. recheck (r, check) Calculate amount of torrent meta files content is found on disk. info (i) Show detailed information about a torrent file. Usage examples can be found in the project documentation on the examples page.","title":"\ud83d\ude80 Usage"},{"location":"#license","text":"Distributed under Apache v2 software license. See LICENSE for more information.","title":"\ud83d\udcdd License"},{"location":"#issues-requests-prs","text":"If you encounter any bugs or would like to request a new feature please open a new issue. PRs and other contributions are welcome https://github.com/alexpdev/torrentfile/issues","title":"\ud83d\udca1 Issues & Requests & PRs"},{"location":"#creating-torrents","text":"Basic torrent file createion > torrentfile create /path/to/content The -t --tracker -a --announce flags add one or more urls to list of trackers. > torrentfile create /path/to/content --tracker http://tracker1.com > torrentfile create -t http://tracker2 http://tracker3 --private /path/to/content > torrentfile create --tracker http://tracker /path/to/content > torrentfile create -t http://tracker1 http://tracker2 /path/to/content the --private flag indicates use by a private tracker the --source flag can be used to help with cross-seeding > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 The options for controlling the progress bar using --prog or --progress : 0 : show no progress bar at all 1 : show progress bar (default) > torrentfile -t http://tracker.com --progress 2 /path/to/content > torrentfile --prog 0 /path/to/content to specify the save location use the -o or --out flags if the path points to directory the name of torrent is autofilled. > torrentfile create -o /specific/path/name.torrent ./content to create files using bittorrent v2 use --meta-version 2 likewise --meta-version 3 creates a hybrid torrent file. > torrentfile create --meta-version 2 /path/to/content > torrentfile create --meta-version 3 /path/to/content","title":"Creating Torrents"},{"location":"#checkrecheck-torrent","text":"recheck torrent file /path/to/some.torrent with /path/to/content > torrentfile recheck /path/to/some.torrent /path/to/content","title":"Check/Recheck Torrent"},{"location":"#edit-torrent","text":"edit a torrent file > torrentfile edit [options] ","title":"Edit Torrent"},{"location":"#create-magnet","text":"To create a magnet URI for a pre-existing torrent meta file, use the sub-command magnet or m with the path to the torrent file. > torrentfile magnet /path/to/some.torrent","title":"Create Magnet"},{"location":"#interactive-mode-expiremental","text":"Alternatively to supplying a bunch of command line arguments, interactive mode allows users to specify program options one at a time from a series of prompts. to activate interactive mode use -i or --interactive flag > torrentfile -i","title":"Interactive Mode (expiremental)"},{"location":"#gui","text":"If you prefer a windowed gui please check out the official GUI frontend here","title":"GUI"},{"location":"Apache2/","text":"Apache License Version 2.0, January 2004 < http://www.apache.org/licenses/ > Terms and Conditions for use, reproduction, and distribution 1. Definitions \u201cLicense\u201d shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. \u201cLicensor\u201d shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. \u201cLegal Entity\u201d shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \u201ccontrol\u201d means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. \u201cYou\u201d (or \u201cYour\u201d) shall mean an individual or Legal Entity exercising permissions granted by this License. \u201cSource\u201d form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. \u201cObject\u201d form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. \u201cWork\u201d shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). \u201cDerivative Works\u201d shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. \u201cContribution\u201d shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \u201csubmitted\u201d means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \u201cNot a Contribution.\u201d \u201cContributor\u201d shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a \u201cNOTICE\u201d text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \u201cAS IS\u201d BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets [] replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \u201cprinted page\u201d as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"License"},{"location":"Apache2/#apache-license","text":"Version 2.0, January 2004 < http://www.apache.org/licenses/ >","title":"Apache License"},{"location":"Apache2/#terms-and-conditions-for-use-reproduction-and-distribution","text":"","title":"Terms and Conditions for use, reproduction, and distribution"},{"location":"Apache2/#1-definitions","text":"\u201cLicense\u201d shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. \u201cLicensor\u201d shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. \u201cLegal Entity\u201d shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \u201ccontrol\u201d means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. \u201cYou\u201d (or \u201cYour\u201d) shall mean an individual or Legal Entity exercising permissions granted by this License. \u201cSource\u201d form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. \u201cObject\u201d form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. \u201cWork\u201d shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). \u201cDerivative Works\u201d shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. \u201cContribution\u201d shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \u201csubmitted\u201d means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \u201cNot a Contribution.\u201d \u201cContributor\u201d shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.","title":"1. Definitions"},{"location":"Apache2/#2-grant-of-copyright-license","text":"Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.","title":"2. Grant of Copyright License"},{"location":"Apache2/#3-grant-of-patent-license","text":"Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.","title":"3. Grant of Patent License"},{"location":"Apache2/#4-redistribution","text":"You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a \u201cNOTICE\u201d text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.","title":"4. Redistribution"},{"location":"Apache2/#5-submission-of-contributions","text":"Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.","title":"5. Submission of Contributions"},{"location":"Apache2/#6-trademarks","text":"This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.","title":"6. Trademarks"},{"location":"Apache2/#7-disclaimer-of-warranty","text":"Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \u201cAS IS\u201d BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.","title":"7. Disclaimer of Warranty"},{"location":"Apache2/#8-limitation-of-liability","text":"In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.","title":"8. Limitation of Liability"},{"location":"Apache2/#9-accepting-warranty-or-additional-liability","text":"While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS","title":"9. Accepting Warranty or Additional Liability"},{"location":"Apache2/#appendix-how-to-apply-the-apache-license-to-your-work","text":"To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets [] replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \u201cprinted page\u201d as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"APPENDIX: How to apply the Apache License to your work"},{"location":"Commands/","text":"TorrentFile CLI Menu Help Messages Main Usage ===== torrentfile [-h] [-i] [-V] [-v] ... CLI Tool for creating, checking, editing... Bittorrent meta files. TorrentFile supports all versions of torrent files. Options ------- -h, --help show this help message and exit -i, --interactive select program options interactively -V, --version show program version and exit -v, --verbose output debug information Actions ------- c (create, new) Create a torrent meta file. e (edit) Edit existing torrent meta file. m (magnet) Create magnet url from an existing Bittorrent meta file. r (recheck, check) Calculate amount of torrent meta file's content is found on disk. i (info) Show detailed information about a torrent file. Create Usage ===== torrentfile c [-h] [-a [ ...]] [-p] [-s ] [-m] [-c ] [-o ] [-t [ ...]] [--noprogress] [--meta-version ] [--piece-length ] [-w [ ...]] Positional Arguments -------------------- Path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit -a [ ...], --announce [ ...] Alias for -t/--tracker -p, --private Create a private torrent file -s , --source Useful for cross-seeding -m, --magnet Output Magnet Link after creation completes -c , --comment Include a comment in file metadata -o , --out Output path for created .torrent file -t [ ...], --tracker [ ...] One or more Bittorrent tracker announce url(s). --prog, --progress (0) = no progress bar displayed (1) = progress bar is displayed (default) --meta-version Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid --piece-length Number of bytes per piece. (Default: None) Acceptable inputs include {14 - 24} as exponent for 2^n, or any acceptable integer value (must be power of 2). Examples:: [--piece-length 14] [--piece-length 16777216] -w [ ...], --web-seed [ ...] One or more url(s) linking to a http server hosting the torrent contents. This is useful if the torrent tracker is ever unreachable. Example:: -w url1 url2 url3 Edit Usage ===== torrentfile e [-h] [--tracker [ ...]] [--web-seed [ ...]] [--private] [--comment ] [--source ] <*.torrent> Positional Arguments -------------------- <*.torrent> path to *.torrent file Optional Arguments ------------------ -h, --help show this help message and exit --tracker [ ...] Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). --web-seed [ ...] Replace current list of web-seed urls with one or more space seperated url(s) --private Make torrent private. --comment Replaces any existing comment with --source Replaces current source with Recheck Usage ===== torrentfile r [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> path to .torrent file. path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit Magnet Usage ===== torrentfile m [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> Path to Bittorrent meta file. Optional Arguments ------------------ -h, --help show this help message and exit","title":"Help Messages"},{"location":"Commands/#torrentfile-cli-menu","text":"","title":"TorrentFile CLI Menu"},{"location":"Commands/#help-messages","text":"","title":"Help Messages"},{"location":"Commands/#main","text":"Usage ===== torrentfile [-h] [-i] [-V] [-v] ... CLI Tool for creating, checking, editing... Bittorrent meta files. TorrentFile supports all versions of torrent files. Options ------- -h, --help show this help message and exit -i, --interactive select program options interactively -V, --version show program version and exit -v, --verbose output debug information Actions ------- c (create, new) Create a torrent meta file. e (edit) Edit existing torrent meta file. m (magnet) Create magnet url from an existing Bittorrent meta file. r (recheck, check) Calculate amount of torrent meta file's content is found on disk. i (info) Show detailed information about a torrent file.","title":"Main"},{"location":"Commands/#create","text":"Usage ===== torrentfile c [-h] [-a [ ...]] [-p] [-s ] [-m] [-c ] [-o ] [-t [ ...]] [--noprogress] [--meta-version ] [--piece-length ] [-w [ ...]] Positional Arguments -------------------- Path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit -a [ ...], --announce [ ...] Alias for -t/--tracker -p, --private Create a private torrent file -s , --source Useful for cross-seeding -m, --magnet Output Magnet Link after creation completes -c , --comment Include a comment in file metadata -o , --out Output path for created .torrent file -t [ ...], --tracker [ ...] One or more Bittorrent tracker announce url(s). --prog, --progress (0) = no progress bar displayed (1) = progress bar is displayed (default) --meta-version Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid --piece-length Number of bytes per piece. (Default: None) Acceptable inputs include {14 - 24} as exponent for 2^n, or any acceptable integer value (must be power of 2). Examples:: [--piece-length 14] [--piece-length 16777216] -w [ ...], --web-seed [ ...] One or more url(s) linking to a http server hosting the torrent contents. This is useful if the torrent tracker is ever unreachable. Example:: -w url1 url2 url3","title":"Create"},{"location":"Commands/#edit","text":"Usage ===== torrentfile e [-h] [--tracker [ ...]] [--web-seed [ ...]] [--private] [--comment ] [--source ] <*.torrent> Positional Arguments -------------------- <*.torrent> path to *.torrent file Optional Arguments ------------------ -h, --help show this help message and exit --tracker [ ...] Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). --web-seed [ ...] Replace current list of web-seed urls with one or more space seperated url(s) --private Make torrent private. --comment Replaces any existing comment with --source Replaces current source with ","title":"Edit"},{"location":"Commands/#recheck","text":"Usage ===== torrentfile r [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> path to .torrent file. path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit","title":"Recheck"},{"location":"Commands/#magnet","text":"Usage ===== torrentfile m [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> Path to Bittorrent meta file. Optional Arguments ------------------ -h, --help show this help message and exit","title":"Magnet"},{"location":"changelog/","text":"TorrentFile Version 0.7.9 complete rewrite of the recheck procedures Recheck now provides more accuracy and more details improvements to the new custom progressbar changed the cli argument for the progress bar the options are now just 0 and 1 included new unit tests for all new features marked unused functions as deprecated added a new hasher object for v2 and hybrid torrents minor bug fixes and styling changes Version 0.7.8 more updates to logging major improvements to progress bar removed tqdm as dependency implemented custom progress bar logic new cli argument controlling the progress bar support for pyben 0.3.1 added threading to recheck module added mixins module unit test updates and improvements Version 0.7.5 updates to logging facility fixed bug in created hybrid torrent files fixed cli when subcomman not chosen doc updates unit test updates and improvements Version 0.7.2 cleaned up readme and help messages removed useless print statements improved CI tooling and checking minor bug fixes Version 0.7.1 split CI integration into separate platform specific files added new cli argument --cwd which changes the default save to location to the current working directory (this will be default in future) added unit tests to cover the new argument Changed license to a the more permissive Apache 2 software license Version 0.7.0 Fixed issues with logging to file. Finished adding tests for Unicode Support Deprecated some unneccessary code Clean up documentation and README removed config files no longer in use. Version 0.6.13 Fixed bug that created a torrent file with no name. Fixed bug that would error if cli path was listed after announce urls Added full unicode support. Added Unittests for new features and bug fixes Version 0.6.11 Fixed bug that occured during recheck when file of 0 length is included. Altered Recheck algorithm to process 0 length files. Only effected meta version 2 and hybrid torrent files. Added unittests to cover the situation. Version 0.6.10 Updates to documentation Integrated Type hints in source code Updated build and CI process Version 0.6.9 The --progress flag is now --noprogress Default behavior is to show progress bar use --noprogress to not show added CLI Help format strings added custom CLI help formatter class Titled Help Message section headers Fixed a bunch of error pages created by mkdocs Version 0.6.8 Documentation for newest features CLI usage examples Improved unittests made progress bar active by default Version 0.6.7 Updates to API Version 0.6.6 bug that created faulty Bittorrent V2 meta files in some instances. back to working as it should. Version 0.6.5 Support for creating Magnet URI's Added optional progress bar for torrent creation Log File handler CLI args page in documentation verbose and logging bugs multi tracker errors bug Version 0.6.4 CLI interface add subcommands added interactive mode Re-wrote the recheck module fixed documentation and docstrings linting and testing errors Version 0.6.3 Fixed Bug that would format list of trackers incorrectly CLI Bug Fixes Version 0.6.2 Bug fixes Documentation error pages Version 0.6.0 cli commands alterations debug logging during creation process Version 0.5.2 Fixed Bug that was adding wrong fields to info dict Version 0.5.0 Slew of new unit tests Stricter linting features Alternative method of -re-check feature Bug Fixes CLI help formatting errors Version 0.4.8 Improved Algorithm performance for ReCheck. Additions to documentation. Version 0.4.7 Fixed A bug that misspelled a field when creating Hybrid torrent files. Re-Check procedure for v2 and hybrid torrent file checking. Version 0.4.6 CLI Help and Usage Messages. Expanded CLI args. Completely new CheckerClass which replaces old Checker Hooks for GUI or other 3rd party apps to hook into Checking Documentation and Unit tests for new CheckerClass Version 0.4.5 Documentation and docstrings improvements Better code formating and more detailed docstrings More unit tests. Version 0.4.2 The ReChecker feature now supports v1, v2, & hybrid .torrent file. Bug in CLI for python < 3.8 Version 0.4.1 Added tests for hybrid class Added logging features new cli flag to activate debug mode Documentation theme. Fixed Bug that allowed improper piece length values. Version 0.4.0 Fixed bugs in creating hybrid files. Bug Fix that broke cli. Version 0.3.0 Added/Improved support for hybrid meta files. Many additions to testing suit including linting and coverage tests. Version 0.2.8 Styling fixes. Bug Fixes. Prelimenary support for bittorrent hybrid meta files. Bug Fixes Version 0.2.7 major imporvements to torrentfile-GUI. minor adjustments to this package for integration. Code consolidation Bug Fixes Documentation additions Implemented CI/CD Integration Version 0.2.3 Bug Fixes Code Style and Formatting Added more unittests Version 0.2.1 Bittorrent Protocol V2 Support v2 metafile options to cli v2 metafile tests Version 0.1.7 Docstrings Improvements. Added documentation rederer. Improved readme file. formatting Version 0.1.2 Added a Command Line Interface Rough Graphical User Interface Minor Bug Fixes Improved unittest coverage Version 0.1.0 added SHA256 support Feeder class for seemless file switching Fixed the primary entrypoint function. Improved docstrings Bug fixes Version 0.0.2 Added Unittests added bencode support added hashing support Version 0.0.1 Initial concept and planning","title":"Changelog"},{"location":"changelog/#torrentfile","text":"","title":"TorrentFile"},{"location":"changelog/#version-079","text":"complete rewrite of the recheck procedures Recheck now provides more accuracy and more details improvements to the new custom progressbar changed the cli argument for the progress bar the options are now just 0 and 1 included new unit tests for all new features marked unused functions as deprecated added a new hasher object for v2 and hybrid torrents minor bug fixes and styling changes","title":"Version 0.7.9"},{"location":"changelog/#version-078","text":"more updates to logging major improvements to progress bar removed tqdm as dependency implemented custom progress bar logic new cli argument controlling the progress bar support for pyben 0.3.1 added threading to recheck module added mixins module unit test updates and improvements","title":"Version 0.7.8"},{"location":"changelog/#version-075","text":"updates to logging facility fixed bug in created hybrid torrent files fixed cli when subcomman not chosen doc updates unit test updates and improvements","title":"Version 0.7.5"},{"location":"changelog/#version-072","text":"cleaned up readme and help messages removed useless print statements improved CI tooling and checking minor bug fixes","title":"Version 0.7.2"},{"location":"changelog/#version-071","text":"split CI integration into separate platform specific files added new cli argument --cwd which changes the default save to location to the current working directory (this will be default in future) added unit tests to cover the new argument Changed license to a the more permissive Apache 2 software license","title":"Version 0.7.1"},{"location":"changelog/#version-070","text":"Fixed issues with logging to file. Finished adding tests for Unicode Support Deprecated some unneccessary code Clean up documentation and README removed config files no longer in use.","title":"Version 0.7.0"},{"location":"changelog/#version-0613","text":"Fixed bug that created a torrent file with no name. Fixed bug that would error if cli path was listed after announce urls Added full unicode support. Added Unittests for new features and bug fixes","title":"Version 0.6.13"},{"location":"changelog/#version-0611","text":"Fixed bug that occured during recheck when file of 0 length is included. Altered Recheck algorithm to process 0 length files. Only effected meta version 2 and hybrid torrent files. Added unittests to cover the situation.","title":"Version 0.6.11"},{"location":"changelog/#version-0610","text":"Updates to documentation Integrated Type hints in source code Updated build and CI process","title":"Version 0.6.10"},{"location":"changelog/#version-069","text":"The --progress flag is now --noprogress Default behavior is to show progress bar use --noprogress to not show added CLI Help format strings added custom CLI help formatter class Titled Help Message section headers Fixed a bunch of error pages created by mkdocs","title":"Version 0.6.9"},{"location":"changelog/#version-068","text":"Documentation for newest features CLI usage examples Improved unittests made progress bar active by default","title":"Version 0.6.8"},{"location":"changelog/#version-067","text":"Updates to API","title":"Version 0.6.7"},{"location":"changelog/#version-066","text":"bug that created faulty Bittorrent V2 meta files in some instances. back to working as it should.","title":"Version 0.6.6"},{"location":"changelog/#version-065","text":"Support for creating Magnet URI's Added optional progress bar for torrent creation Log File handler CLI args page in documentation verbose and logging bugs multi tracker errors bug","title":"Version 0.6.5"},{"location":"changelog/#version-064","text":"CLI interface add subcommands added interactive mode Re-wrote the recheck module fixed documentation and docstrings linting and testing errors","title":"Version 0.6.4"},{"location":"changelog/#version-063","text":"Fixed Bug that would format list of trackers incorrectly CLI Bug Fixes","title":"Version 0.6.3"},{"location":"changelog/#version-062","text":"Bug fixes Documentation error pages","title":"Version 0.6.2"},{"location":"changelog/#version-060","text":"cli commands alterations debug logging during creation process","title":"Version 0.6.0"},{"location":"changelog/#version-052","text":"Fixed Bug that was adding wrong fields to info dict","title":"Version 0.5.2"},{"location":"changelog/#version-050","text":"Slew of new unit tests Stricter linting features Alternative method of -re-check feature Bug Fixes CLI help formatting errors","title":"Version 0.5.0"},{"location":"changelog/#version-048","text":"Improved Algorithm performance for ReCheck. Additions to documentation.","title":"Version 0.4.8"},{"location":"changelog/#version-047","text":"Fixed A bug that misspelled a field when creating Hybrid torrent files. Re-Check procedure for v2 and hybrid torrent file checking.","title":"Version 0.4.7"},{"location":"changelog/#version-046","text":"CLI Help and Usage Messages. Expanded CLI args. Completely new CheckerClass which replaces old Checker Hooks for GUI or other 3rd party apps to hook into Checking Documentation and Unit tests for new CheckerClass","title":"Version 0.4.6"},{"location":"changelog/#version-045","text":"Documentation and docstrings improvements Better code formating and more detailed docstrings More unit tests.","title":"Version 0.4.5"},{"location":"changelog/#version-042","text":"The ReChecker feature now supports v1, v2, & hybrid .torrent file. Bug in CLI for python < 3.8","title":"Version 0.4.2"},{"location":"changelog/#version-041","text":"Added tests for hybrid class Added logging features new cli flag to activate debug mode Documentation theme. Fixed Bug that allowed improper piece length values.","title":"Version 0.4.1"},{"location":"changelog/#version-040","text":"Fixed bugs in creating hybrid files. Bug Fix that broke cli.","title":"Version 0.4.0"},{"location":"changelog/#version-030","text":"Added/Improved support for hybrid meta files. Many additions to testing suit including linting and coverage tests.","title":"Version 0.3.0"},{"location":"changelog/#version-028","text":"Styling fixes. Bug Fixes. Prelimenary support for bittorrent hybrid meta files. Bug Fixes","title":"Version 0.2.8"},{"location":"changelog/#version-027","text":"major imporvements to torrentfile-GUI. minor adjustments to this package for integration. Code consolidation Bug Fixes Documentation additions Implemented CI/CD Integration","title":"Version 0.2.7"},{"location":"changelog/#version-023","text":"Bug Fixes Code Style and Formatting Added more unittests","title":"Version 0.2.3"},{"location":"changelog/#version-021","text":"Bittorrent Protocol V2 Support v2 metafile options to cli v2 metafile tests","title":"Version 0.2.1"},{"location":"changelog/#version-017","text":"Docstrings Improvements. Added documentation rederer. Improved readme file. formatting","title":"Version 0.1.7"},{"location":"changelog/#version-012","text":"Added a Command Line Interface Rough Graphical User Interface Minor Bug Fixes Improved unittest coverage","title":"Version 0.1.2"},{"location":"changelog/#version-010","text":"added SHA256 support Feeder class for seemless file switching Fixed the primary entrypoint function. Improved docstrings Bug fixes","title":"Version 0.1.0"},{"location":"changelog/#version-002","text":"Added Unittests added bencode support added hashing support","title":"Version 0.0.2"},{"location":"changelog/#version-001","text":"Initial concept and planning","title":"Version 0.0.1"},{"location":"man/","text":"Name torrentfile Synopsis torrentfile [options] [options] Description torrentfile is a CLI tool for creating, editing, validating, or reviewing Bittorrent files(.torrent). It supports all current versions of the Bittorrent Protocol files as well as hybrid files. Has support for generating magnet URI's for meta files. Options -h displays all relevant command line options and subcommands. -V displays program and version. -v enables debug mode and outputs a large amount of information to the terminal. -i activates interactive mode for selecting subcommands and options. Sub-commands create alias: c , new torrentfile create [options] torrentfile c [options] The create subcommand is used for generating new torrent files. The only required argument is the path( ) to the contents file or directory. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. --piece-length Uses the number that follows as the piece-length size for the newly created torrent file. If option isn't used the program will pick the ideal size. Acceptable values include 14-29 which is interpreted as the number to raise 2 by (e.g. 14 is interpreted as 16384), or any perfect power of 2 greater than or equal to 16 KiB and less than 1 GiB. Example: --piece-length 14 or --piece-length 16384 . --meta-version Use the following number as the Bittorrent version the torrent will be used on. The default is 1. Options: 1 - Bittorrent v1, 2 - Bittorrent v2, 3 - Bittorrent v1 & v2 Examples: --meta-version 1 , --meta-version 2 , --meta-version 3 -o --out Specify the full path to the newly created torrent file. The default is to save it adjacent to the content. Example: if content is at /home/user/torrents/content the default would create /home/user/torrents/content.torrent --cwd --current Changes the default save location to the current working directory. Example: if content is at /home/user/torrents/content this option would create ./content.torrent --progress --prog Options (0, 1): No status bar will be shown if 0. Otherwise the default and 1 argument means progress bar is shown info alias: i torrentfile info torrentfile i There are no optional arguments for the info subcommand. /path/to/*.torrent The relative or absolute path to the torrent file. edit alias: e torrentfile edit [options] torrentfile e [options] Each option identifies the field to edit inside the torrent file and what the new value should be. If an option is not used then its field will be ommited in the newly created torrent file. As such if the file is marked as private and it should remain that way, the -p option should be used. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. recheck alias: r , check torrentfile recheck torrentfile r There are only two arguments for the recheck command and both are mandatory. The first is the absolute or relative to the torrent file, and the second is the absolute or relative to it's content. This will display a progress bar and at the end output what percent of the torrentfile's content it found at the path indicated. It is also permitted to use the contents parent directory as the second argument and the result will be the same.","title":"Man Page"},{"location":"man/#name","text":"torrentfile","title":"Name"},{"location":"man/#synopsis","text":"torrentfile [options] [options] ","title":"Synopsis"},{"location":"man/#description","text":"torrentfile is a CLI tool for creating, editing, validating, or reviewing Bittorrent files(.torrent). It supports all current versions of the Bittorrent Protocol files as well as hybrid files. Has support for generating magnet URI's for meta files.","title":"Description"},{"location":"man/#options","text":"-h displays all relevant command line options and subcommands. -V displays program and version. -v enables debug mode and outputs a large amount of information to the terminal. -i activates interactive mode for selecting subcommands and options.","title":"Options"},{"location":"man/#sub-commands","text":"","title":"Sub-commands"},{"location":"man/#create","text":"alias: c , new torrentfile create [options] torrentfile c [options] The create subcommand is used for generating new torrent files. The only required argument is the path( ) to the contents file or directory. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. --piece-length Uses the number that follows as the piece-length size for the newly created torrent file. If option isn't used the program will pick the ideal size. Acceptable values include 14-29 which is interpreted as the number to raise 2 by (e.g. 14 is interpreted as 16384), or any perfect power of 2 greater than or equal to 16 KiB and less than 1 GiB. Example: --piece-length 14 or --piece-length 16384 . --meta-version Use the following number as the Bittorrent version the torrent will be used on. The default is 1. Options: 1 - Bittorrent v1, 2 - Bittorrent v2, 3 - Bittorrent v1 & v2 Examples: --meta-version 1 , --meta-version 2 , --meta-version 3 -o --out Specify the full path to the newly created torrent file. The default is to save it adjacent to the content. Example: if content is at /home/user/torrents/content the default would create /home/user/torrents/content.torrent --cwd --current Changes the default save location to the current working directory. Example: if content is at /home/user/torrents/content this option would create ./content.torrent --progress --prog Options (0, 1): No status bar will be shown if 0. Otherwise the default and 1 argument means progress bar is shown","title":"create"},{"location":"man/#info","text":"alias: i torrentfile info torrentfile i There are no optional arguments for the info subcommand. /path/to/*.torrent The relative or absolute path to the torrent file.","title":"info"},{"location":"man/#edit","text":"alias: e torrentfile edit [options] torrentfile e [options] Each option identifies the field to edit inside the torrent file and what the new value should be. If an option is not used then its field will be ommited in the newly created torrent file. As such if the file is marked as private and it should remain that way, the -p option should be used. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT.","title":"edit"},{"location":"man/#recheck","text":"alias: r , check torrentfile recheck torrentfile r There are only two arguments for the recheck command and both are mandatory. The first is the absolute or relative to the torrent file, and the second is the absolute or relative to it's content. This will display a progress bar and at the end output what percent of the torrentfile's content it found at the path indicated. It is also permitted to use the contents parent directory as the second argument and the result will be the same.","title":"recheck"},{"location":"source/","text":"TorrentFile API and Source Torrent Module module torrentfile. torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. Classes MetaFile \u2014 Base Class for all TorrentFile classes. TorrentFile \u2014 Class for creating Bittorrent meta files. TorrentFileV2 \u2014 Class for creating Bittorrent meta v2 files. TorrentFileHybrid \u2014 Construct the Hybrid torrent meta file with provided parameters. TorrentAssembler \u2014 Assembler class for Bittorrent version 2 and hybrid meta files. ------ Utils Module module torrentfile. utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Classes Memo \u2014 Memoice chache object. MissingPathError \u2014 Path parameter is required to specify target content. PieceLengthValueError \u2014 Piece Length parameter must equal a perfect power of 2. Functions get_file_list ( path ) (list) \u2014 Return a sorted list of file paths contained in directory. get_piece_length ( size ) (int) \u2014 Calculate the ideal piece length for bittorrent data. humanize_bytes ( amount ) (str) \u2014 Convert integer into human readable memory sized denomination. next_power_2 ( value ) (int) \u2014 Calculate the next perfect power of 2 equal to or greater than value. normalize_piece_length ( piece_length ) (int) \u2014 Verify input piece_length is valid and convert accordingly. path_piece_length ( path ) (int) \u2014 Calculate piece length for input path and contents. path_size ( path ) (int) \u2014 Return the total size of all files in path recursively. path_stat ( path ) (list) \u2014 Calculate directory statistics. ------ Edit Module module torrentfile. edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds Functions edit_torrent ( metafile , args ) (dict) \u2014 Edit the properties and values in a torrent meta file. filter_empty ( args , meta , info ) \u2014 Remove dictionary keys with empty values. ------ Interactive Module module torrentfile. interactive Module contains the procedures used for Interactive Mode. Classes InteractiveEditor \u2014 Interactive dialog class for torrent editing. InteractiveCreator \u2014 Class namespace for interactive program options. Functions create_torrent ( ) \u2014 Create new torrent file interactively. edit_action ( ) \u2014 Edit the editable values of the torrent meta file. get_input ( *args ) (str) \u2014 Determine appropriate input function to call. recheck_torrent ( ) \u2014 Check torrent download completed percentage. select_action ( ) \u2014 Operate TorrentFile program interactively through terminal. showcenter ( txt ) \u2014 Print text to screen in the center position of the terminal. showtext ( txt ) \u2014 Print contents of txt to screen. ------ CLI Module module torrentfile. cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. Classes TorrentFileHelpFormatter \u2014 Formatting class for help tips provided by the CLI. Functions activate_logger ( ) \u2014 Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. main ( ) \u2014 Initiate main function for CLI script. ------ Recheck Module module torrentfile. recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Classes Checker \u2014 Check a given file or directory to see if it matches a torrentfile. FeedChecker \u2014 Validates torrent content. HashChecker \u2014 Verify that root hashes of content files match the .torrent files. ------ Hasher Module module torrentfile. hasher Piece/File Hashers for Bittorrent meta file contents. Classes Hasher \u2014 Piece hasher for Bittorrent V1 files. HasherV2 \u2014 Calculate the root hash and piece layers for file contents. HasherHybrid \u2014 Calculate root and piece hashes for creating hybrid torrent file. FileHasher \u2014 Calculate root and piece hashes for creating hybrid torrent file. Functions merkle_root ( blocks ) (bytes) \u2014 Calculate the merkle root for a seq of sha256 hash digests. ------ torrentfile Torrentfile can create Bittorrent metafiles for any content. Both Bittorrent v1 and v2 are fully supported. Also included is a torrent torrent file checker, which can verify a .torrent file is formated correctly as well as validate files and folders against metadata. Modules: metafile: Creation/Validation of v1 .torrent files. metafile2: Creation/Validation of v2 .torrent files. torrentfile: torrentfiles Command Line Interface implementation. exceptions: Custom Exceptions used in package. utils: Utilities used throughout package. create ( args ) Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args info ( args ) Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output magnet ( metafile ) Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri __main__ Enable calling the package directly with python from the command line. main () Start the entry point script. Source code in torrentfile\\__main__.py 25 26 27 28 29 def main (): \"\"\" Start the entry point script. \"\"\" execute () cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. TorrentFileHelpFormatter Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts __init__ ( prog , width = 40 , max_help_positions = 30 ) Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) _format_text ( text ) Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" _join_parts ( part_strings ) Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) _split_lines ( text , _ ) Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] format_headers ( parts ) staticmethod Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts activate_logger () Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" ) execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args main () Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute () commands The commands module contains the Action Commands executed by the CLI script. Each function pertains to a command line action/subcommand and drives specific features of the application. Functions create_command info_command edit_command recheck_command magnet_command create ( args ) Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args edit ( args ) Execute the edit CLI sub-command with provided arguments. Parameters: Name Type Description Default args Namespace positional and optional CLI arguments. required Returns: Type Description str path to edited torrent file. Source code in torrentfile\\commands.py 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 def edit ( args : list ): \"\"\" Execute the edit CLI sub-command with provided arguments. Parameters ---------- args : Namespace positional and optional CLI arguments. Returns ------- str path to edited torrent file. \"\"\" metafile = args . metafile logger . info ( \"Editing %s Meta File\" , str ( args . metafile )) editargs = { \"url-list\" : args . url_list , \"httpseeds\" : args . httpseeds , \"announce\" : args . announce , \"source\" : args . source , \"private\" : args . private , \"comment\" : args . comment , } return edit_torrent ( metafile , editargs ) info ( args ) Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output magnet ( metafile ) Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri recheck ( args ) Execute recheck CLI sub-command. Parameters: Name Type Description Default args Namespace positional and optional arguments. required Returns: Type Description str The percentage of content currently saved to disk. Source code in torrentfile\\commands.py 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 def recheck ( args ): \"\"\" Execute recheck CLI sub-command. Parameters ---------- args : Namespace positional and optional arguments. Returns ------- str The percentage of content currently saved to disk. \"\"\" metafile = args . metafile content = args . content logger . debug ( \"Validating %s against %s contents\" , metafile , content ) checker = Checker ( metafile , content ) logger . debug ( \"Completed initialization of the Checker class\" ) result = checker . results () sys . stdout . write ( str ( result ) + \"% Match \\n \" ) sys . stdout . flush () return result edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds edit_torrent ( metafile , args ) Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta filter_empty ( args , meta , info ) Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val ) hasher Piece/File Hashers for Bittorrent meta file contents. FileHasher Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Parameters ---------- data : BytesIO File opened in read mode. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () __init__ ( path , piece_length , progress = True , hybrid = False ) Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) __iter__ () Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self __next__ () Calculate layer hashes for contents of file. Parameters: Name Type Description Default data BytesIO File opened in read mode. required Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Parameters ---------- data : BytesIO File opened in read mode. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash _calculate_root () Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] Hasher Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec __init__ ( paths , piece_length , progress = True ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) __iter__ () Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self __next__ () Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec _handle_partial ( arr ) Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec next_file () Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False HasherHybrid Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) _calculate_root () Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] process_file ( data ) Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () HasherV2 Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) _calculate_root () Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) process_file ( fd ) Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () merkle_root ( blocks ) Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks interactive Module contains the procedures used for Interactive Mode. InteractiveCreator Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () __init__ () Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () get_props () Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () InteractiveEditor Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) __init__ ( metafile ) Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } edit_props () Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) sanatize_response ( key , response ) Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val show_current () Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) _get_input ( txt ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value _get_input_loop ( txt , func ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" ) create_torrent () Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator edit_action () Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props () get_input ( * args ) Determine appropriate input function to call. Parameters: Name Type Description Default args tuple Arbitrary number of args to pass to next function required Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args ) recheck_torrent () Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results select_action () Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover showcenter ( txt ) Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string ) showtext ( txt ) Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt ) mixins Collection of classes that can be used as Mixins with other base classes. Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the progress bar mixin. And any class is eligible to use the callback mixin. CbMixin Mixin class to set a callback during hashing procedure. Source code in torrentfile\\mixins.py 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class CbMixin : \"\"\" Mixin class to set a callback during hashing procedure. \"\"\" _cb = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover set_callback ( func ) classmethod Assign a callback to the Hashing class. Parameters: Name Type Description Default func function the callback function required Source code in torrentfile\\mixins.py 40 41 42 43 44 45 46 47 48 49 50 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover ProgMixin Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods prog_start prog_update prog_close Source code in torrentfile\\mixins.py 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 class ProgMixin : \"\"\" Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods ------- prog_start prog_update prog_close \"\"\" def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start ) def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush () def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False is_active () Test to see if there is an active progress bar for object. Returns: Name Type Description bool True if there is, otherwise False. Source code in torrentfile\\mixins.py 199 200 201 202 203 204 205 206 207 208 209 210 def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False prog_close () Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. Source code in torrentfile\\mixins.py 187 188 189 190 191 192 193 194 195 196 197 def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog prog_start ( total , path , length = 50 , unit = None ) Generate a new progress bar for the given file path. Parameters: Name Type Description Default total int the total amount of units accumulating towards. required path str path to file being hashed. required length int the number of characters of the actual progress bar. 50 unit str the text representation of the value being measured. None Source code in torrentfile\\mixins.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start ) prog_update ( val ) Update progress bar with given amount of progress. Parameters: Name Type Description Default val int the number of bytes count the progress bar should increase. required Source code in torrentfile\\mixins.py 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush () ProgressBar Holds the state and details of the terminal progress bars. Parameters: Name Type Description Default total int the total amount to be accumulated. required title str the subject of the progress tracker required length int the width of the progress bar required unit str the text representation incremented required Source code in torrentfile\\mixins.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 class ProgressBar : \"\"\" Holds the state and details of the terminal progress bars. Parameters ---------- total : int the total amount to be accumulated. title : str the subject of the progress tracker length : int the width of the progress bar unit : str the text representation incremented \"\"\" def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ]) def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar ) __init__ ( total , title , length , unit , start ) Construct the progress bar object and store state of it's properties. Source code in torrentfile\\mixins.py 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ]) increment ( value ) Increase the state of the progress bar value. Parameters: Name Type Description Default value int the amount to increment the state by. required Source code in torrentfile\\mixins.py 96 97 98 99 100 101 102 103 104 105 def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value pbar () Return the size of the filled portion of the progress bar. Returns: Name Type Description str the progress bar characters Source code in torrentfile\\mixins.py 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar ) waiting ( msg , flag , timeout = 180 ) Show loading message while thread completes processing. Parameters: Name Type Description Default msg str Message string printed before the progress bar required flag list Once flag is filled exit loop required timeout int max amount of time to run the function. 180 Source code in torrentfile\\mixins.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 def waiting ( msg , flag , timeout = 180 ): \"\"\" Show loading message while thread completes processing. Parameters ---------- msg : str Message string printed before the progress bar flag : list Once flag is filled exit loop timeout : int max amount of time to run the function. \"\"\" then = time . time () codes , fill = list ( range ( 9617 , 9620 )), chr ( 9619 ) size = idx = 0 total = shutil . get_terminal_size () . columns - len ( msg ) - 20 def output ( text ): \"\"\" Print parameter message to the console. Parameters ---------- text : str output message \"\"\" sys . stdout . write ( text ) sys . stdout . flush () output ( \" \\n \" ) while not flag : time . sleep ( 0.16 ) filled = ( fill * size ) + chr ( codes [ idx ]) + ( \" \" * ( total - size )) output ( f \" { msg } : { filled } \\r \" ) idx = idx + 1 if idx + 1 < len ( codes ) else 0 size = size + 1 if size < total else 0 if time . time () - then > timeout : break output ( \" \\n \" ) recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Checker Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required Example >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 __init__ ( metafile , path ) Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () check_paths () Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) find_root ( path ) Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) iter_hashes () Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 log_msg ( * args , level = logging . INFO ) Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default args dict formatting args for log message required level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) piece_checker () Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker register_callback ( hook ) classmethod Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook results () Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result walk_file_tree ( tree , partials ) Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) FeedChecker Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required hasher Any hashing class for calculating piece hashes. default=None required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. hasher : Any hashing class for calculating piece hashes. default=None \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial __init__ ( checker ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 350 351 352 353 354 355 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self __next__ () Yield back result of comparison. Source code in torrentfile\\recheck.py 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) _gen_padding ( partial , length , read = 0 ) Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial extract ( path , partial ) Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad iter_pieces () Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () HashChecker Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required hasher Object the version specific hashing class for torrent content. required Source code in torrentfile\\recheck.py 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. hasher : Object the version specific hashing class for torrent content. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size Padder Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( length , piece_length ) Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 534 535 536 537 538 539 540 541 542 543 544 545 546 547 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () __iter__ () Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 549 550 551 552 553 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover __next__ () Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( checker ) Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 491 492 493 494 495 496 497 498 499 500 501 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 503 504 505 506 507 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self __next__ () Provide the result of comparison. Source code in torrentfile\\recheck.py 509 510 511 512 513 514 515 516 517 518 519 520 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr advance () Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size next_file () Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False process_current () Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. MetaFile Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta __init__ ( path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ ) Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name assemble () Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError set_callback ( func ) classmethod Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) sort_meta () Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta write ( outfile = None ) Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta TorrentAssembler Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent options. required Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFile Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default kwargs dict Dictionary containing torrent file options. required Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces __init__ ( ** kwargs ) Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default kwargs dict dictionary of keyword args passed to superclass. required Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () assemble () Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces TorrentFileHybrid Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent options. required Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFileV2 Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent file options. required Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree __init__ ( ** kwargs ) Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default kwargs dict keywword arguments to pass to superclass. required Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree assemble () Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Memo Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __call__ ( path ) Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __init__ ( func ) Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} MissingPathError Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) PieceLengthValueError Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) _filelist_total ( path ) Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist ) filelist_total ( pathstring ) Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError get_file_list ( path ) Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist get_piece_length ( size ) Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp humanize_bytes ( amount ) Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\" next_power_2 ( value ) Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start normalize_piece_length ( piece_length ) Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError path_piece_length ( path ) Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize ) path_size ( path ) Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size path_stat ( path ) Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length ) version Holds the release version number. torrentfile.cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. TorrentFileHelpFormatter Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts __init__ ( prog , width = 40 , max_help_positions = 30 ) Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) _format_text ( text ) Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" _join_parts ( part_strings ) Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) _split_lines ( text , _ ) Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] format_headers ( parts ) staticmethod Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts activate_logger () Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" ) execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args main () Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute () torrentfile.edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds edit_torrent ( metafile , args ) Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta filter_empty ( args , meta , info ) Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val ) torrentfile.hasher Piece/File Hashers for Bittorrent meta file contents. FileHasher Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Parameters ---------- data : BytesIO File opened in read mode. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () __init__ ( path , piece_length , progress = True , hybrid = False ) Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) __iter__ () Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self __next__ () Calculate layer hashes for contents of file. Parameters: Name Type Description Default data BytesIO File opened in read mode. required Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Parameters ---------- data : BytesIO File opened in read mode. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash _calculate_root () Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] Hasher Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec __init__ ( paths , piece_length , progress = True ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) __iter__ () Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self __next__ () Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec _handle_partial ( arr ) Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec next_file () Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False HasherHybrid Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) _calculate_root () Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] process_file ( data ) Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () HasherV2 Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) _calculate_root () Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) process_file ( fd ) Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () merkle_root ( blocks ) Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks torrentfile.interactive Module contains the procedures used for Interactive Mode. InteractiveCreator Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () __init__ () Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () get_props () Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () InteractiveEditor Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) __init__ ( metafile ) Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } edit_props () Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) sanatize_response ( key , response ) Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val show_current () Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) _get_input ( txt ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value _get_input_loop ( txt , func ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" ) create_torrent () Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator edit_action () Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props () get_input ( * args ) Determine appropriate input function to call. Parameters: Name Type Description Default args tuple Arbitrary number of args to pass to next function required Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args ) recheck_torrent () Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results select_action () Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover showcenter ( txt ) Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string ) showtext ( txt ) Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt ) torrentfile.recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Checker Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required Example >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 __init__ ( metafile , path ) Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () check_paths () Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) find_root ( path ) Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) iter_hashes () Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 log_msg ( * args , level = logging . INFO ) Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default args dict formatting args for log message required level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) piece_checker () Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker register_callback ( hook ) classmethod Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook results () Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result walk_file_tree ( tree , partials ) Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) FeedChecker Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required hasher Any hashing class for calculating piece hashes. default=None required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. hasher : Any hashing class for calculating piece hashes. default=None \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial __init__ ( checker ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 350 351 352 353 354 355 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self __next__ () Yield back result of comparison. Source code in torrentfile\\recheck.py 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) _gen_padding ( partial , length , read = 0 ) Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial extract ( path , partial ) Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad iter_pieces () Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () HashChecker Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required hasher Object the version specific hashing class for torrent content. required Source code in torrentfile\\recheck.py 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. hasher : Object the version specific hashing class for torrent content. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size Padder Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( length , piece_length ) Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 534 535 536 537 538 539 540 541 542 543 544 545 546 547 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () __iter__ () Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 549 550 551 552 553 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover __next__ () Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( checker ) Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 491 492 493 494 495 496 497 498 499 500 501 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 503 504 505 506 507 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self __next__ () Provide the result of comparison. Source code in torrentfile\\recheck.py 509 510 511 512 513 514 515 516 517 518 519 520 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr advance () Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size next_file () Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False process_current () Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err torrentfile.torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. MetaFile Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta __init__ ( path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ ) Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name assemble () Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError set_callback ( func ) classmethod Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) sort_meta () Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta write ( outfile = None ) Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta TorrentAssembler Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent options. required Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFile Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default kwargs dict Dictionary containing torrent file options. required Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces __init__ ( ** kwargs ) Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default kwargs dict dictionary of keyword args passed to superclass. required Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () assemble () Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces TorrentFileHybrid Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent options. required Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFileV2 Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent file options. required Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree __init__ ( ** kwargs ) Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default kwargs dict keywword arguments to pass to superclass. required Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree assemble () Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers torrentfile.utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Memo Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __call__ ( path ) Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __init__ ( func ) Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} MissingPathError Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) PieceLengthValueError Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) _filelist_total ( path ) Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist ) filelist_total ( pathstring ) Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError get_file_list ( path ) Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist get_piece_length ( size ) Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp humanize_bytes ( amount ) Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\" next_power_2 ( value ) Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start normalize_piece_length ( piece_length ) Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError path_piece_length ( path ) Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize ) path_size ( path ) Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size path_stat ( path ) Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length ) torrentfile.version Holds the release version number. tests Unittest package init module. dir1 () Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 168 169 170 171 172 173 174 175 176 177 178 179 @pytest . fixture ( scope = \"package\" ) def dir1 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir () yield root rmpath ( root ) dir2 () Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 182 183 184 185 186 187 188 189 190 191 192 193 @pytest . fixture () def dir2 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir ( ext = \"2\" ) yield Path ( root ) rmpath ( root ) file1 () Return the path to a temporary file package scope. Source code in tests\\__init__.py 241 242 243 244 245 246 247 248 @pytest . fixture ( scope = \"package\" ) def file1 (): \"\"\" Return the path to a temporary file package scope. \"\"\" path = tempfile () yield path rmpath ( path ) file2 () Return the path to a temporary file no scope. Source code in tests\\__init__.py 297 298 299 300 301 302 303 304 @pytest . fixture () def file2 (): \"\"\" Return the path to a temporary file no scope. \"\"\" path = tempfile () yield path rmpath ( path ) filemeta1 ( file1 , request ) Test fixture for generating metafile for all versions of torrents. Source code in tests\\__init__.py 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 @pytest . fixture ( scope = \"package\" , params = torrents ()) def filemeta1 ( file1 , request ): \"\"\" Test fixture for generating metafile for all versions of torrents. \"\"\" args = { \"path\" : file1 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file1 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile ) filemeta2 ( file2 , request ) Test fixture for generating a meta file no scope. Source code in tests\\__init__.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 @pytest . fixture ( params = torrents ()) def filemeta2 ( file2 , request ): \"\"\" Test fixture for generating a meta file no scope. \"\"\" args = { \"path\" : file2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url7\" , \"url8\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file2 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile ) metafile1 ( dir1 , request ) Create a standard metafile for testing. Source code in tests\\__init__.py 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 @pytest . fixture ( scope = \"package\" , params = torrents ()) def metafile1 ( dir1 , request ): \"\"\" Create a standard metafile for testing. \"\"\" versions = torrents () args = { \"path\" : dir1 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir1 ) + str ( versions . index ( torrent_class )) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile ) metafile2 ( dir2 , request ) Create a standard metafile for testing. Source code in tests\\__init__.py 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 @pytest . fixture ( params = torrents ()) def metafile2 ( dir2 , request ): \"\"\" Create a standard metafile for testing. \"\"\" args = { \"path\" : dir2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"httpseeds\" : [ \"url6\" , \"url7\" ], \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir2 ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile ) rmpath ( * args ) Remove file or directory path. Parameters: Name Type Description Default args list Filesystem locations for removing. required Source code in tests\\__init__.py 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def rmpath ( * args ): \"\"\"Remove file or directory path. Parameters ---------- args : list Filesystem locations for removing. \"\"\" for arg in args : if not os . path . exists ( arg ): continue if os . path . isdir ( arg ): try : shutil . rmtree ( arg ) except PermissionError : # pragma: nocover pass elif os . path . isfile ( arg ): try : os . remove ( arg ) except PermissionError : # pragma: nocover pass sizedfiles ( dir2 , sizes , request ) Generate variable sized meta files for testing, no scope. Source code in tests\\__init__.py 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 @pytest . fixture ( params = torrents ()) def sizedfiles ( dir2 , sizes , request ): \"\"\" Generate variable sized meta files for testing, no scope. \"\"\" versions = torrents () args = { \"content\" : dir2 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , \"piece_length\" : sizes , } torrent_class = request . param version = str ( versions . index ( torrent_class )) outfile = str ( dir2 ) + version + str ( sizes ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile ) sizes ( request ) Generate powers of 2 for file creation package scope. Source code in tests\\__init__.py 159 160 161 162 163 164 165 @pytest . fixture ( scope = \"package\" , params = [ 2 ** i for i in range ( 15 , 20 )]) def sizes ( request ): \"\"\" Generate powers of 2 for file creation package scope. \"\"\" size = request . param yield size teardown () Remove all temporary directories and files. Source code in tests\\__init__.py 141 142 143 144 145 146 147 148 149 @atexit . register def teardown (): # pragma: nocover \"\"\" Remove all temporary directories and files. \"\"\" root = Path ( __file__ ) . parent / \"TESTDIR\" for path in [ root , \"./torrentfile.log\" ]: if os . path . exists ( path ): rmpath ( path ) tempdir ( ext = '1' ) Create temporary directory. Parameters: Name Type Description Default ext str , optional extension to file names, by default \"1\" '1' Returns: Type Description str path to common root for directory. Source code in tests\\__init__.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 def tempdir ( ext = \"1\" ): \"\"\"Create temporary directory. Parameters ---------- ext : str, optional extension to file names, by default \"1\" Returns ------- str path to common root for directory. \"\"\" layouts = { \"1\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.mp4\" , f \"dir { ext } /file3.mp3\" , f \"dir { ext } /file4.zip\" , f \"dir { ext } /file5.txt\" , f \"dir { ext } /subdir1/subdir2/file.7z\" , f \"dir { ext } /subdir/subdir/file4.rar\" , f \"dir { ext } /subdir/subdir/file4.r01\" , ], \"2\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.jpg\" , f \"dir { ext } /subdir/file2.mp4\" , f \"dir { ext } /subdir/file3.mp3\" , ], } paths = [] for path in layouts [ ext ]: temps = tempfile ( path = path , exp = 18 ) paths . append ( temps ) return os . path . commonpath ( paths ) tempfile ( path = None , exp = 18 ) Create temporary file. Creates a temporary file for unittesting purposes.py Parameters: Name Type Description Default path str , optional relative path to temporary files, by default None None exp int , optional Exponent used to determine size of file., by default 18 18 Returns: Type Description str absolute path to file. Source code in tests\\__init__.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 def tempfile ( path = None , exp = 18 ): \"\"\"Create temporary file. Creates a temporary file for unittesting purposes.py Parameters ---------- path : str, optional relative path to temporary files, by default None exp : int, optional Exponent used to determine size of file., by default 18 Returns ------- str absolute path to file. \"\"\" seq = ( string . printable + string . whitespace ) . encode ( \"utf-8\" ) root = Path ( __file__ ) . parent / \"TESTDIR\" if not os . path . exists ( root ): os . mkdir ( root ) if not path : path = root / ( str ( datetime . timestamp ( datetime . now ())) + \".file\" ) parts = Path ( path ) . parts partial = root for i , part in enumerate ( parts ): partial = partial / part if i == len ( parts ) - 1 : with open ( partial , \"wb\" ) as binfile : size = 2 ** exp while size > 0 : if len ( seq ) < size : binfile . write ( seq ) size -= len ( seq ) seq += seq else : binfile . write ( seq [: size ]) size -= size else : if not os . path . exists ( partial ): os . mkdir ( partial ) return partial torrents () Return seq of torrentfile objects. Source code in tests\\__init__.py 152 153 154 155 156 def torrents (): \"\"\" Return seq of torrentfile objects. \"\"\" return [ TorrentFile , TorrentFileV2 , TorrentFileHybrid , TorrentAssembler ] test_cli Testing functions for the command line interface. folder ( dir1 ) Yield a folder object as fixture. Source code in tests\\test_cli.py 41 42 43 44 45 46 47 48 49 @pytest . fixture ( scope = \"module\" ) def folder ( dir1 ): \"\"\" Yield a folder object as fixture. \"\"\" sfolder = str ( dir1 ) torrent = sfolder + \".torrent\" yield ( sfolder , torrent ) rmpath ( torrent ) test_cli_announce ( folder , piece_length , version ) Test announce cli flag. Source code in tests\\test_cli.py 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce ( folder , piece_length , version ): \"\"\" Test announce cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--tracker\" , \"https://announce.org/tracker\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"announce\" ] == \"https://announce.org/tracker\" test_cli_announce_list ( folder , version ) Test announce-list cli flag. Source code in tests\\test_cli.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce_list ( folder , version ): \"\"\" Test announce-list cli flag. \"\"\" folder , torrent = folder trackers = [ \"https://announce.org/tracker\" , \"https://announce.net/tracker\" , \"https://tracker.net/announce\" , ] args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , version , \"--tracker\" , ] + trackers sys . argv = args execute () meta = pyben . load ( torrent ) for url in trackers : assert url in [ j for i in meta [ \"announce-list\" ] for j in i ] test_cli_announce_path ( dir1 , flag ) Test CLI when path is placed after the trackers flag. Source code in tests\\test_cli.py 451 452 453 454 455 456 457 458 459 460 461 462 463 @pytest . mark . parametrize ( \"flag\" , [ \"-t\" , \"-w\" , \"--announce\" , \"--web-seed\" , \"--http-seed\" ] ) def test_cli_announce_path ( dir1 , flag ): \"\"\" Test CLI when path is placed after the trackers flag. \"\"\" args = [ \"torrentfile\" , \"create\" , flag , \"https://announce1.org\" , str ( dir1 )] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_comment ( folder , piece_length , version ) Test comment cli flag. Source code in tests\\test_cli.py 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_comment ( folder , piece_length , version ): \"\"\" Test comment cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--magnet\" , \"--comment\" , \"this is a comment\" , \"--progress\" , \"1\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"comment\" ] == \"this is a comment\" test_cli_created_by ( folder , piece_length , version ) Test if created torrents recieve a created by field in meta info. Source code in tests\\test_cli.py 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_created_by ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a created by field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"TorrentFile\" in meta [ \"created by\" ] test_cli_creation_date ( folder , piece_length , version ) Test if torrents created get an accurate timestamp. Source code in tests\\test_cli.py 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_creation_date ( folder , piece_length , version ): \"\"\" Test if torrents created get an accurate timestamp. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) num = float ( meta [ \"creation date\" ]) date = datetime . datetime . fromtimestamp ( num ) now = datetime . datetime . now () assert date . day == now . day assert date . year == now . year assert date . month == now . month test_cli_cwd ( folder ) Test outfile cli flag. Source code in tests\\test_cli.py 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 def test_cli_cwd ( folder ): \"\"\" Test outfile cli flag. \"\"\" folder , _ = folder args = [ \"torrentfile\" , \"create\" , \"--cwd\" , folder , ] sys . argv = args current = os . getcwd () name = os . path . basename ( folder ) outfile = os . path . join ( current , name ) + \".torrent\" execute () assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_empty_files ( dir2 , version , progress ) Test creating torrent with empty files. Source code in tests\\test_cli.py 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"progress\" , [ \"0\" , \"1\" ]) def test_cli_empty_files ( dir2 , version , progress ): \"\"\" Test creating torrent with empty files. \"\"\" args = [ \"torrentfile\" , \"create\" , str ( dir2 ), \"--meta-version\" , version , \"--source\" , \"somesource\" , \"--prog\" , progress , ] sys . argv = args def walk ( root , count ): \"\"\" Traverse directory to edit files. \"\"\" if root . is_file (): with open ( root , \"wb\" ) as _ : return 1 elif root . is_dir (): for item in root . iterdir (): if count >= 2 : break count += walk ( item , count ) return count walk ( dir2 , 0 ) execute () outfile = str ( dir2 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_help () Test showing help notice cli flag. Source code in tests\\test_cli.py 352 353 354 355 356 357 358 359 360 361 def test_cli_help (): \"\"\" Test showing help notice cli flag. \"\"\" args = [ \"-h\" ] sys . argv = args try : assert execute () except SystemExit : assert True test_cli_outfile ( dir1 , piece_length , version ) Test outfile cli flag. Source code in tests\\test_cli.py 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_outfile ( dir1 , piece_length , version ): \"\"\" Test outfile cli flag. \"\"\" outfile = dir1 + \"test.torrent\" args = [ \"torrentfile\" , \"create\" , dir1 , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-o\" , outfile , \"--prog\" , \"1\" , ] sys . argv = args execute () assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_piece_length ( folder , piece_length , version ) Test piece length cli flag. Source code in tests\\test_cli.py 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_piece_length ( folder , piece_length , version ): \"\"\" Test piece length cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--progress\" , \"0\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"piece length\" ] == piece_length test_cli_private ( folder ) Test private cli flag. Source code in tests\\test_cli.py 85 86 87 88 89 90 91 92 93 94 def test_cli_private ( folder ): \"\"\" Test private cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--private\" ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"private\" in meta [ \"info\" ] test_cli_slash_outpath ( dir1 , sep ) Test if output when outpath ends with a /. Source code in tests\\test_cli.py 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 @pytest . mark . parametrize ( \"sep\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_outpath ( dir1 , sep ): \"\"\" Test if output when outpath ends with a /. \"\"\" if sys . platform != \"win32\" : sep = \"/\" # pragma: nocover parent = os . path . dirname ( dir1 ) + sep args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , \"-o\" , parent , str ( dir1 ), ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_slash_path ( dir1 , ending ) Test if output when path ends with a /. Source code in tests\\test_cli.py 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 @pytest . mark . parametrize ( \"ending\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_path ( dir1 , ending ): \"\"\" Test if output when path ends with a /. \"\"\" if sys . platform != \"win32\" and ending == \" \\\\ \" : # pragma: nocover ending = \"/\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , str ( dir1 ) + ending , ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_v1 ( folder ) Basic create torrent cli command. Source code in tests\\test_cli.py 52 53 54 55 56 57 58 59 60 def test_cli_v1 ( folder ): \"\"\" Basic create torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_v2 ( folder ) Create torrent v2 cli command. Source code in tests\\test_cli.py 63 64 65 66 67 68 69 70 71 def test_cli_v2 ( folder ): \"\"\" Create torrent v2 cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"2\" ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_v3 ( folder ) Create hybrid torrent cli command. Source code in tests\\test_cli.py 74 75 76 77 78 79 80 81 82 def test_cli_v3 ( folder ): \"\"\" Create hybrid torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"3\" ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_web_seeds ( folder , piece_length , version ) Test if created torrents recieve a web seeds field in meta info. Source code in tests\\test_cli.py 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_web_seeds ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a web seeds field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-w\" , \"https://webseed.url/1\" , \"https://webseed.url/2\" , \"https://webseed.url/3\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"https://webseed.url/1\" in meta [ \"url-list\" ] test_cli_with_debug ( folder , piece_length , version ) Test debug mode cli flag. Source code in tests\\test_cli.py 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_debug ( folder , piece_length , version ): \"\"\" Test debug mode cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_with_source ( folder , piece_length , version ) Test source cli flag. Source code in tests\\test_cli.py 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_source ( folder , piece_length , version ): \"\"\" Test source cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--source\" , \"somesource\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"source\" ] == \"somesource\" test_fix () Test dir1 fixture is not None. Source code in tests\\test_cli.py 34 35 36 37 38 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and dir2 test_commands Testing functions for the sub-action commands from command line args. test_create_unicode_name ( file1 ) Test Unicode information in CLI args. Source code in tests\\test_commands.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 def test_create_unicode_name ( file1 ): \"\"\" Test Unicode information in CLI args. \"\"\" parent = os . path . dirname ( file1 ) filename = os . path . join ( parent , \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" ) args = [ \"torrentfile\" , \"-v\" , \"create\" , \"-a\" , \"tracker_url.com/announce_3456\" , \"tracker_url.net/announce_3456\" , \"--source\" , \"sourcetext\" , \"--comment\" , \"filename is \u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"-o\" , str ( filename ), str ( file1 ), ] sys . argv = args execute () assert os . path . exists ( filename ) test_fix () Test dir1 fixture is not None. Source code in tests\\test_commands.py 37 38 39 40 41 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and metafile1 and file1 and metafile2 and dir2 test_info ( field , file1 ) Test the info_command action from the Command Line Interface. Source code in tests\\test_commands.py 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 @pytest . mark . parametrize ( \"field\" , [ \"name\" , \"announce\" , \"source\" , \"comment\" , \"private\" , \"announce-list\" ], ) def test_info ( field , file1 ): \"\"\" Test the info_command action from the Command Line Interface. \"\"\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"url1\" , \"url2\" , \"url3\" , \"--web-seed\" , \"url4\" , \"url5\" , \"--http-seed\" , \"url6\" , \"url7\" , \"--private\" , \"--comment\" , \"ExampleComment\" , \"--source\" , \"examplesource\" , str ( file1 ), ] sys . argv = args execute () class Space : \"\"\" Stand in substitution for argparse.Namespace object. \"\"\" metafile = str ( file1 ) + \".torrent\" output = info ( Space ) assert field in output test_magnet ( metafile1 ) Test create magnet function scheme. Source code in tests\\test_commands.py 65 66 67 68 69 70 def test_magnet ( metafile1 ): \"\"\" Test create magnet function scheme. \"\"\" magnet_link = magnet ( metafile1 ) assert magnet_link . startswith ( \"magnet\" ) test_magnet_cli ( metafile1 ) Test magnet creation through CLI interface. Source code in tests\\test_commands.py 136 137 138 139 140 141 142 def test_magnet_cli ( metafile1 ): \"\"\" Test magnet creation through CLI interface. \"\"\" sys . argv [ 1 :] = [ \"m\" , str ( metafile1 )] uri = execute () assert \"magnet\" in uri test_magnet_empty () Test create magnet function scheme. Source code in tests\\test_commands.py 84 85 86 87 88 89 90 91 def test_magnet_empty (): \"\"\" Test create magnet function scheme. \"\"\" try : magnet ( \"file_that_does_not_exist\" ) except FileNotFoundError : assert True test_magnet_hex ( metafile1 ) Test create magnet function digest. Source code in tests\\test_commands.py 54 55 56 57 58 59 60 61 62 def test_magnet_hex ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) info = meta [ \"info\" ] binfo = sha1 ( pyben . dumps ( info )) . hexdigest () . upper () assert binfo in magnet_link test_magnet_no_announce_list ( metafile2 ) Test create magnet function scheme. Source code in tests\\test_commands.py 73 74 75 76 77 78 79 80 81 def test_magnet_no_announce_list ( metafile2 ): \"\"\" Test create magnet function scheme. \"\"\" meta = pyben . load ( metafile2 ) del meta [ \"announce-list\" ] pyben . dump ( meta , metafile2 ) magnet_link = magnet ( metafile2 ) assert magnet_link . startswith ( \"magnet\" ) test_magnet_uri ( metafile1 ) Test create magnet function digest. Source code in tests\\test_commands.py 44 45 46 47 48 49 50 51 def test_magnet_uri ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) announce = meta [ \"announce\" ] assert quote_plus ( announce ) in magnet_link test_merkle_root_no_blocks ( blocks ) Test running merkle root function with 1 and 0 len lists. Source code in tests\\test_commands.py 171 172 173 174 175 176 177 178 179 @pytest . mark . parametrize ( \"blocks\" , [[], [ sha1 ( b \"1010\" ) . digest ()]]) # nosec def test_merkle_root_no_blocks ( blocks ): \"\"\" Test running merkle root function with 1 and 0 len lists. \"\"\" if blocks : assert merkle_root ( blocks ) else : assert not merkle_root ( blocks ) test_mixins_progbar ( torrent ) Test progbar mixins with small file. Source code in tests\\test_commands.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 @pytest . mark . parametrize ( \"torrent\" , torrents ()) def test_mixins_progbar ( torrent ): \"\"\" Test progbar mixins with small file. \"\"\" tfile = tempfile ( exp = 14 ) msg = \"1234abcd\" * 80 with open ( tfile , \"wb\" ) as temp : temp . write ( msg . encode ( \"utf-8\" )) args = { \"path\" : tfile , \"--prog\" : \"1\" , } metafile = torrent ( ** args ) output , _ = metafile . write () assert output == str ( tfile ) + \".torrent\" rmpath ( tfile ) test_edit Testing the edit torrent feature. test_edit_cli ( metafile2 , comment , source , announce , webseed , httpseed ) Test edit torrent with all params on cli. Source code in tests\\test_edit.py 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 @pytest . mark . parametrize ( \"comment\" , [ \"commenta\" , \"commentb\" , \"commentc\" ]) @pytest . mark . parametrize ( \"source\" , [ \"sourcea\" , \"sourceb\" , \"sourcec\" ]) @pytest . mark . parametrize ( \"announce\" , [[ \"url1\" , \"url2\" , \"url3\" ], [ \"url1\" ]]) @pytest . mark . parametrize ( \"webseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) @pytest . mark . parametrize ( \"httpseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) def test_edit_cli ( metafile2 , comment , source , announce , webseed , httpseed ): \"\"\" Test edit torrent with all params on cli. \"\"\" sys . argv = [ \"torrentfile\" , \"edit\" , metafile2 , \"--comment\" , comment , \"--source\" , source , \"--web-seed\" , webseed , \"--http-seed\" , httpseed , \"--tracker\" , announce , \"--private\" , ] main () meta = pyben . load ( metafile2 ) info = meta [ \"info\" ] assert comment == info . get ( \"comment\" ) assert source == info . get ( \"source\" ) assert info . get ( \"private\" ) == 1 assert meta [ \"announce-list\" ] == [[ announce ]] assert meta [ \"url-list\" ] == [ webseed ] test_edit_comment ( metafile2 , comment ) Test edit torrent with comment param. Source code in tests\\test_edit.py 118 119 120 121 122 123 124 125 126 127 @pytest . mark . parametrize ( \"comment\" , [ \"COMMENT\" , \"COMIT\" , \"MITCO\" ]) def test_edit_comment ( metafile2 , comment ): \"\"\" Test edit torrent with comment param. \"\"\" edits = { \"comment\" : comment } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"comment\" ] == comment test_edit_httpseeds ( metafile2 , httpseed ) Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 104 105 106 107 108 109 110 111 112 113 114 115 @pytest . mark . parametrize ( \"httpseed\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_httpseeds ( metafile2 , httpseed ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"httpseeds\" : httpseed } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseed test_edit_httpseeds_str ( metafile2 , httpseeds ) Test edit torrent with webseed param. Source code in tests\\test_edit.py 78 79 80 81 82 83 84 85 86 87 @pytest . mark . parametrize ( \"httpseeds\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_httpseeds_str ( metafile2 , httpseeds ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"httpseeds\" : httpseeds } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseeds . split () test_edit_none ( metafile2 ) Test edit torrent with None for all params. Source code in tests\\test_edit.py 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 def test_edit_none ( metafile2 ): \"\"\" Test edit torrent with None for all params. \"\"\" edits = { \"announce\" : None , \"url-list\" : None , \"comment\" : None , \"source\" : None , \"private\" : None , } data = pyben . load ( metafile2 ) edited = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta == edited test_edit_private_false ( metafile2 ) Test edit torrent with private param False. Source code in tests\\test_edit.py 153 154 155 156 157 158 159 160 161 def test_edit_private_false ( metafile2 ): \"\"\" Test edit torrent with private param False. \"\"\" edits = { \"private\" : \"\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert \"private\" not in data [ \"info\" ] test_edit_private_true ( metafile2 ) Test edit torrent with private param. Source code in tests\\test_edit.py 142 143 144 145 146 147 148 149 150 def test_edit_private_true ( metafile2 ): \"\"\" Test edit torrent with private param. \"\"\" edits = { \"private\" : \"1\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"private\" ] == 1 test_edit_removal ( metafile2 ) Test edit torrent with empty for all params. Source code in tests\\test_edit.py 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_edit_removal ( metafile2 ): \"\"\" Test edit torrent with empty for all params. \"\"\" edits = { \"announce\" : \"\" , \"url-list\" : \"\" , \"httpseeds\" : \"\" , \"comment\" : \"\" , \"source\" : \"\" , \"private\" : \"\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta test_edit_source ( metafile2 , source ) Test edit torrent with source param. Source code in tests\\test_edit.py 130 131 132 133 134 135 136 137 138 139 @pytest . mark . parametrize ( \"source\" , [ \"SomeSource\" , \"NoSouce\" , \"MidSource\" ]) def test_edit_source ( metafile2 , source ): \"\"\" Test edit torrent with source param. \"\"\" edits = { \"source\" : source } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"source\" ] == source test_edit_torrent ( metafile2 , announce ) Test edit torrent with announce param. Source code in tests\\test_edit.py 40 41 42 43 44 45 46 47 48 49 50 51 @pytest . mark . parametrize ( \"announce\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_torrent ( metafile2 , announce ): \"\"\" Test edit torrent with announce param. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce ] test_edit_torrent_str ( metafile2 , announce ) Test edit torrent with announce param as string. Source code in tests\\test_edit.py 54 55 56 57 58 59 60 61 62 63 @pytest . mark . parametrize ( \"announce\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_torrent_str ( metafile2 , announce ): \"\"\" Test edit torrent with announce param as string. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce . split ()] test_edit_urllist ( metafile2 , url_list ) Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 90 91 92 93 94 95 96 97 98 99 100 101 @pytest . mark . parametrize ( \"url_list\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_urllist ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list test_edit_urllist_str ( metafile2 , url_list ) Test edit torrent with webseed param. Source code in tests\\test_edit.py 66 67 68 69 70 71 72 73 74 75 @pytest . mark . parametrize ( \"url_list\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_urllist_str ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list . split () test_fix () Testing dir fixtures. Source code in tests\\test_edit.py 33 34 35 36 37 def test_fix (): \"\"\" Testing dir fixtures. \"\"\" assert dir2 and metafile2 and dir1 test_metafile_edit_with_unicode ( metafile2 ) Test if editing full unicode works as it should. Source code in tests\\test_edit.py 233 234 235 236 237 238 239 240 241 242 243 244 245 246 def test_metafile_edit_with_unicode ( metafile2 ): \"\"\" Test if editing full unicode works as it should. \"\"\" edits = { \"comment\" : \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"source\" : \"\u4e02\u4e03\u4e07\u4e0f\u4e11\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) com1 = data [ \"info\" ][ \"comment\" ] com2 = meta [ \"info\" ][ \"comment\" ] msg = edits [ \"comment\" ] assert com1 == com2 == msg test_interactive Testing functions for the command line interface. test_fixtures () Test the fixtures used in module. Source code in tests\\test_interactive.py 37 38 39 40 41 def test_fixtures (): \"\"\" Test the fixtures used in module. \"\"\" assert filemeta2 and file1 and file2 test_inter_create_full ( file1 , piece_length , announce , comment , source , url_list , version , monkeypatch ) Test creating torrent interactively with many parameters. Source code in tests\\test_interactive.py 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"piece_length\" , [ \"23\" , \"18\" , \"131072\" ]) @pytest . mark . parametrize ( \"announce\" , [ \"url1\" , \"urla urlb urlc\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Do\" , \"Ra\" , \"Me\" ]) def test_inter_create_full ( file1 , piece_length , announce , comment , source , url_list , version , monkeypatch , ): \"\"\" Test creating torrent interactively with many parameters. \"\"\" mapping = [ \"create\" , piece_length , announce , url_list , url_list , comment , source , \"Y\" , file1 , str ( file1 ) + \".torrent\" , version , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta = pyben . load ( str ( file1 ) + \".torrent\" ) assert meta [ \"info\" ][ \"source\" ] == source assert meta [ \"info\" ][ \"piece length\" ] == normalize_piece_length ( piece_length ) assert meta [ \"info\" ][ \"comment\" ] == comment assert meta [ \"url-list\" ] == url_list . split () test_inter_edit_cli ( filemeta2 , announce , cmnt , srce , urllist , monkeypatch ) Test editing torrent interactively from CLI. Source code in tests\\test_interactive.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 @pytest . mark . parametrize ( \"announce\" , [ \"urla urlb urlc\" , \"urld url2\" ]) @pytest . mark . parametrize ( \"urllist\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"cmnt\" , [ \"Some Comment\" ]) @pytest . mark . parametrize ( \"srce\" , [ \"Do\" , \"Ra\" ]) def test_inter_edit_cli ( filemeta2 , announce , cmnt , srce , urllist , monkeypatch ): \"\"\" Test editing torrent interactively from CLI. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , cmnt , \"2\" , srce , \"5\" , urllist , urllist , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) sys . argv = [ \"torrentfile\" , \"-i\" ] main () meta2 = pyben . load ( filemeta2 ) assert meta2 [ \"info\" ][ \"source\" ] == srce assert meta2 [ \"info\" ][ \"comment\" ] == cmnt assert meta2 [ \"url-list\" ] == urllist . split () assert meta2 [ \"info\" ][ \"private\" ] == 1 test_inter_edit_full ( filemeta2 , announce , comment , source , url_list , monkeypatch ) Test editing torrent file interactively. Source code in tests\\test_interactive.py 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 @pytest . mark . parametrize ( \"announce\" , [ \"url1\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Fa\" , \"So\" , \"La\" ]) def test_inter_edit_full ( filemeta2 , announce , comment , source , url_list , monkeypatch ): \"\"\" Test editing torrent file interactively. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , comment , \"2\" , source , \"5\" , url_list , \"\" , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta1 = pyben . load ( filemeta2 ) assert meta1 [ \"info\" ][ \"source\" ] == source assert meta1 [ \"info\" ][ \"comment\" ] == comment assert meta1 [ \"url-list\" ] == url_list . split () assert meta1 [ \"info\" ][ \"private\" ] == 1 test_inter_recheck ( torrentclass , monkeypatch , file1 ) Test interactive recheck function. Source code in tests\\test_interactive.py 180 181 182 183 184 185 186 187 188 189 190 191 @pytest . mark . parametrize ( \"torrentclass\" , torrents ()) def test_inter_recheck ( torrentclass , monkeypatch , file1 ): \"\"\" Test interactive recheck function. \"\"\" torrent = torrentclass ( path = file1 ) filemeta , _ = torrent . write () seq = [ \"recheck\" , filemeta , str ( file1 )] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) result = select_action () assert result == 100 test_interactive_create ( monkeypatch , file1 ) Test creating torrent interactively. Source code in tests\\test_interactive.py 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 def test_interactive_create ( monkeypatch , file1 ): \"\"\" Test creating torrent interactively. \"\"\" mapping = [ \"create\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , file1 , str ( file1 ) + \".torrent\" , \"\" , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () assert os . path . exists ( str ( file1 ) + \".torrent\" ) test_recheck Testing functions for the progress module. test_checker_callback ( dir1 , metafile1 ) Test Checker class with directory that points to nothing. Source code in tests\\test_recheck.py 125 126 127 128 129 130 131 def test_checker_callback ( dir1 , metafile1 ): \"\"\" Test Checker class with directory that points to nothing. \"\"\" Checker . register_callback ( lambda * x : print ( x )) checker = Checker ( metafile1 , str ( dir1 )) assert checker . results () == 100 test_checker_class ( dir1 , metafile1 ) Test Checker Class against meta files. Source code in tests\\test_recheck.py 42 43 44 45 46 47 def test_checker_class ( dir1 , metafile1 ): \"\"\" Test Checker Class against meta files. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100 test_checker_class_allfiles ( sizedfiles , dir2 ) Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 def test_checker_class_allfiles ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" def traverse ( path ): \"\"\" Traverse internal subdirectories. \"\"\" if path . is_file (): rmpath ( path ) elif path . is_dir (): for item in path . iterdir (): traverse ( item ) traverse ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_class_allpaths ( sizedfiles , dir2 ) Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 211 212 213 214 215 216 217 218 def test_checker_class_allpaths ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" for item in Path ( str ( dir2 )) . iterdir (): rmpath ( item ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_class_half_file ( filemeta2 , file2 ) Test Checker class with half size single file. Source code in tests\\test_recheck.py 221 222 223 224 225 226 227 228 229 230 231 232 def test_checker_class_half_file ( filemeta2 , file2 ): \"\"\" Test Checker class with half size single file. \"\"\" half = int ( os . path . getsize ( file2 ) / 2 ) barr = bytearray ( half ) with open ( file2 , \"rb\" ) as content : content . readinto ( barr ) with open ( file2 , \"wb\" ) as content : content . write ( barr ) checker = Checker ( filemeta2 , file2 ) assert int ( checker . results ()) != 10 test_checker_cli_args ( dir1 , metafile1 ) Test exclusive Checker Mode CLI. Source code in tests\\test_recheck.py 134 135 136 137 138 139 140 def test_checker_cli_args ( dir1 , metafile1 ): \"\"\" Test exclusive Checker Mode CLI. \"\"\" sys . argv = [ \"torrentfile\" , \"check\" , str ( metafile1 ), str ( dir1 )] output = main () assert output == 100 test_checker_empty_files ( dir2 , sizedfiles ) Test Checker when directory contains 0 length files. Source code in tests\\test_recheck.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 def test_checker_empty_files ( dir2 , sizedfiles ): \"\"\" Test Checker when directory contains 0 length files. \"\"\" def empty_files ( root ): \"\"\" Dump contents of files. \"\"\" if os . path . isfile ( root ): with open ( root , \"wb\" ) as _ : pass assert os . path . getsize ( root ) == 0 elif os . path . isdir ( root ): for item in os . listdir ( root ): return empty_files ( os . path . join ( root , item )) return root empty_files ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100 test_checker_first_piece ( dir2 , sizedfiles ) Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 def test_checker_first_piece ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if path . is_file (): new = b \"Something other than what was there before.\" with open ( path , \"rb\" ) as bfile : data = bfile . read () new_len = len ( new ) content = b \"\" . join ([ new , data [ new_len :]]) with open ( path , \"wb\" ) as bdoc : bdoc . write ( content ) elif path . is_dir (): for item in path . iterdir (): change ( item ) change ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100 test_checker_first_piece_alt ( dir2 , sizedfiles ) Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 def test_checker_first_piece_alt ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if os . path . isfile ( path ): with open ( path , \"rb\" ) as bfile : data = bfile . read () new = b \"some_other_bytes_to_use\" new_len = len ( new ) with open ( path , \"wb\" ) as wfile : wfile . write ( new + data [ new_len :]) elif os . path . isdir ( path ): for item in os . listdir ( path ): change ( os . path . join ( path , item )) change ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100 test_checker_missing ( sizedfiles , dir2 ) Test Checker class when files are missing from contents. Source code in tests\\test_recheck.py 179 180 181 182 183 184 185 186 187 188 def test_checker_missing ( sizedfiles , dir2 ): \"\"\" Test Checker class when files are missing from contents. \"\"\" count = 0 for fd in Path ( dir2 ) . iterdir (): if fd . is_file () and count < 2 : rmpath ( fd ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_missing_singles ( dir2 , sizedfiles ) Test Checker class with half size single file. Source code in tests\\test_recheck.py 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def test_checker_missing_singles ( dir2 , sizedfiles ): \"\"\" Test Checker class with half size single file. \"\"\" def walk ( root ): \"\"\" Remove first file found. \"\"\" if root . is_file (): rmpath ( root ) return True if root . is_dir (): for item in root . iterdir (): walk ( item ) return False walk ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_no_meta_file () Test Checker when incorrect metafile is provided. Source code in tests\\test_recheck.py 159 160 161 162 163 164 165 166 def test_checker_no_meta_file (): \"\"\" Test Checker when incorrect metafile is provided. \"\"\" try : Checker ( \"peaches\" , \"$\" ) except FileNotFoundError : assert True test_checker_parent_dir ( dir1 , metafile1 ) Test providing the parent directory for torrent checking feature. Source code in tests\\test_recheck.py 143 144 145 146 147 148 def test_checker_parent_dir ( dir1 , metafile1 ): \"\"\" Test providing the parent directory for torrent checking feature. \"\"\" checker = Checker ( metafile1 , os . path . dirname ( dir1 )) assert checker . results () == 100 test_checker_result_property ( dir1 , metafile1 ) Test Checker class with half size single file. Source code in tests\\test_recheck.py 257 258 259 260 261 262 263 def test_checker_result_property ( dir1 , metafile1 ): \"\"\" Test Checker class with half size single file. \"\"\" checker = Checker ( metafile1 , dir1 ) result = checker . results () assert checker . results () == result test_checker_simplest ( dir1 , metafile1 ) Test the simplest example. Source code in tests\\test_recheck.py 266 267 268 269 270 271 def test_checker_simplest ( dir1 , metafile1 ): \"\"\" Test the simplest example. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100 test_checker_with_file ( file1 , filemeta1 ) Test checker with single file torrent. Source code in tests\\test_recheck.py 151 152 153 154 155 156 def test_checker_with_file ( file1 , filemeta1 ): \"\"\" Test checker with single file torrent. \"\"\" checker = Checker ( filemeta1 , file1 ) assert checker . results () == 100 test_checker_wrong_root_dir ( metafile1 ) Test Checker when incorrect root directory is provided. Source code in tests\\test_recheck.py 169 170 171 172 173 174 175 176 def test_checker_wrong_root_dir ( metafile1 ): \"\"\" Test Checker when incorrect root directory is provided. \"\"\" try : Checker ( metafile1 , \"fake\" ) except FileNotFoundError : assert True test_fixtures () Test fixtures exist. Source code in tests\\test_recheck.py 33 34 35 36 37 38 39 def test_fixtures (): \"\"\" Test fixtures exist. \"\"\" assert dir1 and dir2 and file1 and file2 assert filemeta1 and filemeta2 and metafile1 assert metafile2 and sizes and sizedfiles test_partial_metafiles ( dir2 , sizedfiles ) Test Checker with data that is expected to be incomplete. Source code in tests\\test_recheck.py 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 def test_partial_metafiles ( dir2 , sizedfiles ): \"\"\" Test Checker with data that is expected to be incomplete. \"\"\" def shortenfile ( path ): \"\"\" Shorten a few files for testing purposes. \"\"\" with open ( path , \"rb\" ) as bfile : data = bfile . read () with open ( path , \"wb\" ) as bfile : bfile . write ( data [: - ( 2 ** 10 )]) for item in os . listdir ( dir2 ): full = os . path . join ( dir2 , item ) if os . path . isfile ( full ): shortenfile ( full ) testdir = os . path . dirname ( dir2 ) checker = Checker ( sizedfiles , testdir ) assert checker . results () != 100 test_recheck_wrong_dir ( metafile1 ) Test recheck function with directory that doesn't contain the contents. Source code in tests\\test_recheck.py 297 298 299 300 301 302 303 304 305 def test_recheck_wrong_dir ( metafile1 ): \"\"\" Test recheck function with directory that doesn't contain the contents. \"\"\" grandparent = os . path . dirname ( os . path . dirname ( metafile1 )) try : _ = Checker ( metafile1 , grandparent ) except FileNotFoundError : assert True test_torrent Testing functions for the torrent module. test_create_cwd_fail () Test cwd argument with create command failure. Source code in tests\\test_torrent.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_create_cwd_fail (): \"\"\"Test cwd argument with create command failure.\"\"\" class SuFile : \"\"\"A mock admin file.\"\"\" @staticmethod def __fspath__ (): raise PermissionError def __str__ ( self ): return \"SuFile\" tfile = tempfile () name = os . path . basename ( tfile ) + \".torrent\" torrent = MetaFile ( path = tfile ) sufile = SuFile () torrent . write ( outfile = sufile ) current = os . path . join ( \".\" , name ) assert os . path . exists ( current ) rmpath ( tfile , current ) test_fixtures () Test pytest fixtures. Source code in tests\\test_torrent.py 32 33 34 35 36 def test_fixtures (): \"\"\" Test pytest fixtures. \"\"\" assert dir1 and dir2 test_mbtorrent ( version , progress ) Test torrent creation for file size larger than 10MB. Source code in tests\\test_torrent.py 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 @pytest . mark . parametrize ( \"version\" , torrents ()) @pytest . mark . parametrize ( \"progress\" , [ 0 , 1 , 2 ]) def test_mbtorrent ( version , progress ): \"\"\" Test torrent creation for file size larger than 10MB. \"\"\" tfile = tempfile ( exp = 26 ) args = { \"path\" : tfile , \"progress\" : progress , \"piece_length\" : \"14\" , } torrent = version ( ** args ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile ) test_metafile_assemble ( dir1 ) Test assembling base metafile exception. Source code in tests\\test_torrent.py 50 51 52 53 54 55 56 57 58 def test_metafile_assemble ( dir1 ): \"\"\" Test assembling base metafile exception. \"\"\" metafile = MetaFile ( path = dir1 ) try : metafile . assemble () except NotImplementedError : assert True test_torrentfile_extra ( dir2 , version ) Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_extra ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" def walk ( item ): \"\"\" Edit files in directory structure. \"\"\" if item . is_file (): with open ( item , \"ab\" ) as binfile : binfile . write ( bytes ( 1000 )) elif item . is_dir (): for sub in item . iterdir (): walk ( sub ) walk ( dir2 ) args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\" test_torrentfile_missing_path ( version ) Test missing path error exception. Source code in tests\\test_torrent.py 39 40 41 42 43 44 45 46 47 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_missing_path ( version ): \"\"\" Test missing path error exception. \"\"\" try : version () except MissingPathError : assert True test_torrentfile_one_empty ( dir2 , version ) Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_one_empty ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" a = next ( os . walk ( dir2 )) if len ( a [ - 1 ]) > 0 : with open ( os . path . join ( a [ 0 ], a [ - 1 ][ 0 ]), \"w\" , encoding = \"utf-8\" ) as _ : pass args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\" test_torrentfile_single ( version , num , piece_length , capsys ) Test creating a torrent file from a single file contents. Source code in tests\\test_torrent.py 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 @pytest . mark . parametrize ( \"num\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single ( version , num , piece_length , capsys ): \"\"\" Test creating a torrent file from a single file contents. \"\"\" tfile = tempfile ( exp = num ) with capsys . disabled (): version . set_callback ( print ) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } trent = version ( ** args ) trent . write () assert os . path . exists ( str ( tfile ) + \".torrent\" ) rmpath ( tfile , str ( tfile ) + \".torrent\" ) test_torrentfile_single_extra ( version , size , piece_length ) Test creating a torrent file from a single file contents plus extra. Source code in tests\\test_torrent.py 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 @pytest . mark . parametrize ( \"size\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single_extra ( version , size , piece_length ): \"\"\" Test creating a torrent file from a single file contents plus extra. \"\"\" tfile = tempfile ( exp = size ) with open ( tfile , \"ab\" ) as binfile : binfile . write ( bytes ( str ( tfile ) . encode ( \"utf-8\" ))) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } torrent = version ( ** args ) torrent . write () outfile = str ( tfile ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( tfile , outfile ) test_torrentfile_single_under ( ver , sze , piecelength ) Test creating a torrent file from less than a single file contents. Source code in tests\\test_torrent.py 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 @pytest . mark . parametrize ( \"sze\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piecelength\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"ver\" , torrents ()) def test_torrentfile_single_under ( ver , sze , piecelength ): \"\"\" Test creating a torrent file from less than a single file contents. \"\"\" tfile = tempfile ( exp = sze ) with open ( tfile , \"rb\" ) as binfile : data = binfile . read () with open ( tfile , \"wb\" ) as binfile : binfile . write ( data [: - ( 2 ** 9 )]) kwargs = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piecelength , } torrent = ver ( ** kwargs ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile ) test_waiting_mixin () Test waiting function. Source code in tests\\test_torrent.py 198 199 200 201 202 203 204 205 206 def test_waiting_mixin (): \"\"\" Test waiting function. \"\"\" msg = \"Testing message\" lst = [] timeout = 3 waiting ( msg , lst , timeout = timeout ) assert len ( lst ) == 0 test_utils Unittest functions for testing torrentfile utils module. test_filelist_total ( dir1 ) Test function for acquiring a filelist for directory. Source code in tests\\test_utils.py 118 119 120 121 122 123 def test_filelist_total ( dir1 ): \"\"\" Test function for acquiring a filelist for directory. \"\"\" total , _ = utils . filelist_total ( dir1 ) assert total == ( 2 ** 18 ) * 8 test_filelisttotal_missing ( dir2 ) Test function filelist total with missing path. Parameters: Name Type Description Default dir2 pytest . fixture fixture containing a temporary directory required Source code in tests\\test_utils.py 225 226 227 228 229 230 231 232 233 234 235 236 237 def test_filelisttotal_missing ( dir2 ): \"\"\"Test function filelist total with missing path. Parameters ---------- dir2 : pytest.fixture fixture containing a temporary directory \"\"\" rmpath ( dir2 ) try : utils . filelist_total ( dir2 ) except utils . MissingPathError : assert True test_get_filelist ( dir1 ) Test function for get a list of files in a directory. Source code in tests\\test_utils.py 102 103 104 105 106 107 def test_get_filelist ( dir1 ): \"\"\" Test function for get a list of files in a directory. \"\"\" filelist = utils . get_file_list ( dir1 ) assert len ( filelist ) == 8 test_get_path_length_max ( dir1 ) Test function for getting piece length for folders max. Source code in tests\\test_utils.py 71 72 73 74 75 def test_get_path_length_max ( dir1 ): \"\"\" Test function for getting piece length for folders max. \"\"\" assert utils . path_piece_length ( dir1 ) <= ( 2 ** 27 ) test_get_path_length_min ( dir1 ) Test function for getting piece length for folders min. Source code in tests\\test_utils.py 64 65 66 67 68 def test_get_path_length_min ( dir1 ): \"\"\" Test function for getting piece length for folders min. \"\"\" assert utils . path_piece_length ( dir1 ) >= ( 2 ** 14 ) test_get_path_length_mod ( dir1 ) Test function for the best piece length for provided path. Source code in tests\\test_utils.py 57 58 59 60 61 def test_get_path_length_mod ( dir1 ): \"\"\" Test function for the best piece length for provided path. \"\"\" assert utils . path_piece_length ( dir1 ) % ( 2 ** 14 ) == 0 test_get_path_size ( dir1 ) Test function for getting total size of directory. Source code in tests\\test_utils.py 110 111 112 113 114 115 def test_get_path_size ( dir1 ): \"\"\" Test function for getting total size of directory. \"\"\" pathsize = utils . path_size ( dir1 ) assert pathsize == ( 2 ** 18 ) * 8 test_get_piece_length ( size ) Test function for best piece length for given size. Source code in tests\\test_utils.py 30 31 32 33 34 35 36 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length ( size ): \"\"\" Test function for best piece length for given size. \"\"\" value = utils . get_piece_length ( size ) assert value % 1024 == 0 test_get_piece_length_max ( size ) Test function for best piece length for given size maximum. Source code in tests\\test_utils.py 39 40 41 42 43 44 45 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_max ( size ): \"\"\" Test function for best piece length for given size maximum. \"\"\" value = utils . get_piece_length ( size ) assert value < 2 ** 27 test_get_piece_length_min ( size ) Test function for best piece length for given size minimum. Source code in tests\\test_utils.py 48 49 50 51 52 53 54 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_min ( size ): \"\"\" Test function for best piece length for given size minimum. \"\"\" value = utils . get_piece_length ( size ) assert value >= 2 ** 14 test_humanize_bytes ( amount , result ) Test humanize bytes function. Source code in tests\\test_utils.py 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 @pytest . mark . parametrize ( \"amount, result\" , [ ( 100 , \"100\" ), ( 1100 , \"1 KiB\" ), ( 1_100_000 , \"1 MiB\" ), ( 1_100_000_000 , \"1 GiB\" ), ( 4_400_120_000 , \"4 GiB\" ), ( 4_000_120_000 , \"3 GiB\" ), ], ) def test_humanize_bytes ( amount , result ): \"\"\" Test humanize bytes function. \"\"\" assert utils . humanize_bytes ( amount ) == result test_missing_path_error () Test exception for missing path parameter. Source code in tests\\test_utils.py 137 138 139 140 141 142 143 144 145 def test_missing_path_error (): \"\"\" Test exception for missing path parameter. \"\"\" try : raise utils . MissingPathError ( \"message\" ) except utils . MissingPathError : assert True assert dir2 test_next_power_2 ( value ) Test next power of 2 function in utils module. Source code in tests\\test_utils.py 148 149 150 151 152 153 154 155 156 157 @pytest . mark . parametrize ( \"value\" , [ 5 , 32 , 18 , 225 , 16384 , 256000 ]) def test_next_power_2 ( value ): \"\"\" Test next power of 2 function in utils module. \"\"\" result = utils . next_power_2 ( value ) log = math . log2 ( result ) assert log == int ( log ) assert result % 2 == 0 assert result >= value test_norm_plength_errors ( amount ) Test function to normalize piece length errors. Parameters: Name Type Description Default amount any arguments intended to raise an exception. required Source code in tests\\test_utils.py 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"amount\" , [ \"hello\" , 11 , 0 , 100000 , 28 , \"zero\" , \"fifteen\" ] ) def test_norm_plength_errors ( amount ): \"\"\"Test function to normalize piece length errors. Parameters ---------- amount : any arguments intended to raise an exception. \"\"\" try : assert utils . normalize_piece_length ( amount ) except utils . PieceLengthValueError : assert True test_normalize_piece_length_int ( amount , result ) Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 178 179 180 181 182 183 184 185 186 187 188 189 @pytest . mark . parametrize ( \"amount, result\" , [( i , 2 ** i ) for i in range ( 14 , 25 )]) def test_normalize_piece_length_int ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result test_normalize_piece_length_str ( amount , result ) Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 192 193 194 195 196 197 198 199 200 201 202 203 204 205 @pytest . mark . parametrize ( \"amount, result\" , [( str ( i ), 2 ** i ) for i in range ( 14 , 21 )] ) def test_normalize_piece_length_str ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result test_path_stat ( dir1 ) Test function for acquiring piece length information on folder. Source code in tests\\test_utils.py 78 79 80 81 82 83 def test_path_stat ( dir1 ): \"\"\" Test function for acquiring piece length information on folder. \"\"\" _ , _ , piece_length = utils . path_stat ( dir1 ) assert piece_length % ( 2 ** 14 ) == 0 test_path_stat_filelist_size ( dir1 ) Test function for acquiring file list information on folder. Source code in tests\\test_utils.py 94 95 96 97 98 99 def test_path_stat_filelist_size ( dir1 ): \"\"\" Test function for acquiring file list information on folder. \"\"\" filelist , _ , _ = utils . path_stat ( dir1 ) assert len ( filelist ) == 8 test_path_stat_size ( dir1 ) Test function for acquiring total size information on folder. Source code in tests\\test_utils.py 86 87 88 89 90 91 def test_path_stat_size ( dir1 ): \"\"\" Test function for acquiring total size information on folder. \"\"\" _ , totalsize , _ = utils . path_stat ( dir1 ) assert totalsize == ( 2 ** 18 ) * 8 test_piecelength_error_fixtures () Test exception for uninterpretable piece length value. Source code in tests\\test_utils.py 126 127 128 129 130 131 132 133 134 def test_piecelength_error_fixtures (): \"\"\" Test exception for uninterpretable piece length value. \"\"\" try : raise utils . PieceLengthValueError ( \"message\" ) except utils . PieceLengthValueError : assert True assert dir1","title":"Source Code"},{"location":"source/#torrentfile_1","text":"","title":"TorrentFile"},{"location":"source/#api-and-source","text":"","title":"API and Source"},{"location":"source/#torrent-module","text":"module torrentfile. torrent Classes and procedures pertaining to the creation of torrent meta files.","title":"Torrent Module"},{"location":"source/#classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. Classes MetaFile \u2014 Base Class for all TorrentFile classes. TorrentFile \u2014 Class for creating Bittorrent meta files. TorrentFileV2 \u2014 Class for creating Bittorrent meta v2 files. TorrentFileHybrid \u2014 Construct the Hybrid torrent meta file with provided parameters. TorrentAssembler \u2014 Assembler class for Bittorrent version 2 and hybrid meta files.","title":"v1 meta-dictionary"},{"location":"source/#-","text":"","title":"------"},{"location":"source/#utils-module","text":"module torrentfile. utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Classes Memo \u2014 Memoice chache object. MissingPathError \u2014 Path parameter is required to specify target content. PieceLengthValueError \u2014 Piece Length parameter must equal a perfect power of 2. Functions get_file_list ( path ) (list) \u2014 Return a sorted list of file paths contained in directory. get_piece_length ( size ) (int) \u2014 Calculate the ideal piece length for bittorrent data. humanize_bytes ( amount ) (str) \u2014 Convert integer into human readable memory sized denomination. next_power_2 ( value ) (int) \u2014 Calculate the next perfect power of 2 equal to or greater than value. normalize_piece_length ( piece_length ) (int) \u2014 Verify input piece_length is valid and convert accordingly. path_piece_length ( path ) (int) \u2014 Calculate piece length for input path and contents. path_size ( path ) (int) \u2014 Return the total size of all files in path recursively. path_stat ( path ) (list) \u2014 Calculate directory statistics.","title":"Utils Module"},{"location":"source/#-_1","text":"","title":"------"},{"location":"source/#edit-module","text":"module torrentfile. edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"Edit Module"},{"location":"source/#keywords","text":"private comment source trackers web-seeds Functions edit_torrent ( metafile , args ) (dict) \u2014 Edit the properties and values in a torrent meta file. filter_empty ( args , meta , info ) \u2014 Remove dictionary keys with empty values.","title":"Keywords"},{"location":"source/#-_2","text":"","title":"------"},{"location":"source/#interactive-module","text":"module torrentfile. interactive Module contains the procedures used for Interactive Mode. Classes InteractiveEditor \u2014 Interactive dialog class for torrent editing. InteractiveCreator \u2014 Class namespace for interactive program options. Functions create_torrent ( ) \u2014 Create new torrent file interactively. edit_action ( ) \u2014 Edit the editable values of the torrent meta file. get_input ( *args ) (str) \u2014 Determine appropriate input function to call. recheck_torrent ( ) \u2014 Check torrent download completed percentage. select_action ( ) \u2014 Operate TorrentFile program interactively through terminal. showcenter ( txt ) \u2014 Print text to screen in the center position of the terminal. showtext ( txt ) \u2014 Print contents of txt to screen.","title":"Interactive Module"},{"location":"source/#-_3","text":"","title":"------"},{"location":"source/#cli-module","text":"module torrentfile. cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. Classes TorrentFileHelpFormatter \u2014 Formatting class for help tips provided by the CLI. Functions activate_logger ( ) \u2014 Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. main ( ) \u2014 Initiate main function for CLI script.","title":"CLI Module"},{"location":"source/#-_4","text":"","title":"------"},{"location":"source/#recheck-module","text":"module torrentfile. recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Classes Checker \u2014 Check a given file or directory to see if it matches a torrentfile. FeedChecker \u2014 Validates torrent content. HashChecker \u2014 Verify that root hashes of content files match the .torrent files.","title":"Recheck Module"},{"location":"source/#-_5","text":"","title":"------"},{"location":"source/#hasher-module","text":"module torrentfile. hasher Piece/File Hashers for Bittorrent meta file contents. Classes Hasher \u2014 Piece hasher for Bittorrent V1 files. HasherV2 \u2014 Calculate the root hash and piece layers for file contents. HasherHybrid \u2014 Calculate root and piece hashes for creating hybrid torrent file. FileHasher \u2014 Calculate root and piece hashes for creating hybrid torrent file. Functions merkle_root ( blocks ) (bytes) \u2014 Calculate the merkle root for a seq of sha256 hash digests.","title":"Hasher Module"},{"location":"source/#-_6","text":"","title":"------"},{"location":"source/#torrentfile","text":"Torrentfile can create Bittorrent metafiles for any content. Both Bittorrent v1 and v2 are fully supported. Also included is a torrent torrent file checker, which can verify a .torrent file is formated correctly as well as validate files and folders against metadata. Modules: metafile: Creation/Validation of v1 .torrent files. metafile2: Creation/Validation of v2 .torrent files. torrentfile: torrentfiles Command Line Interface implementation. exceptions: Custom Exceptions used in package. utils: Utilities used throughout package.","title":"torrentfile"},{"location":"source/#torrentfile.create","text":"Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args","title":"create()"},{"location":"source/#torrentfile.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args","title":"execute()"},{"location":"source/#torrentfile.info","text":"Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output","title":"info()"},{"location":"source/#torrentfile.magnet","text":"Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri","title":"magnet()"},{"location":"source/#torrentfile.__main__","text":"Enable calling the package directly with python from the command line.","title":"__main__"},{"location":"source/#torrentfile.__main__.main","text":"Start the entry point script. Source code in torrentfile\\__main__.py 25 26 27 28 29 def main (): \"\"\" Start the entry point script. \"\"\" execute ()","title":"main()"},{"location":"source/#torrentfile.cli","text":"Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility.","title":"cli"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter","text":"Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"TorrentFileHelpFormatter"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.__init__","text":"Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions )","title":"__init__()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._format_text","text":"Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \"","title":"_format_text()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._join_parts","text":"Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts )","title":"_join_parts()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._split_lines","text":"Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ]","title":"_split_lines()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.format_headers","text":"Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"format_headers()"},{"location":"source/#torrentfile.cli.activate_logger","text":"Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" )","title":"activate_logger()"},{"location":"source/#torrentfile.cli.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args","title":"execute()"},{"location":"source/#torrentfile.cli.main","text":"Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute ()","title":"main()"},{"location":"source/#torrentfile.commands","text":"The commands module contains the Action Commands executed by the CLI script. Each function pertains to a command line action/subcommand and drives specific features of the application.","title":"commands"},{"location":"source/#torrentfile.commands--functions","text":"create_command info_command edit_command recheck_command magnet_command","title":"Functions"},{"location":"source/#torrentfile.commands.create","text":"Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args","title":"create()"},{"location":"source/#torrentfile.commands.edit","text":"Execute the edit CLI sub-command with provided arguments. Parameters: Name Type Description Default args Namespace positional and optional CLI arguments. required Returns: Type Description str path to edited torrent file. Source code in torrentfile\\commands.py 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 def edit ( args : list ): \"\"\" Execute the edit CLI sub-command with provided arguments. Parameters ---------- args : Namespace positional and optional CLI arguments. Returns ------- str path to edited torrent file. \"\"\" metafile = args . metafile logger . info ( \"Editing %s Meta File\" , str ( args . metafile )) editargs = { \"url-list\" : args . url_list , \"httpseeds\" : args . httpseeds , \"announce\" : args . announce , \"source\" : args . source , \"private\" : args . private , \"comment\" : args . comment , } return edit_torrent ( metafile , editargs )","title":"edit()"},{"location":"source/#torrentfile.commands.info","text":"Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output","title":"info()"},{"location":"source/#torrentfile.commands.magnet","text":"Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri","title":"magnet()"},{"location":"source/#torrentfile.commands.recheck","text":"Execute recheck CLI sub-command. Parameters: Name Type Description Default args Namespace positional and optional arguments. required Returns: Type Description str The percentage of content currently saved to disk. Source code in torrentfile\\commands.py 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 def recheck ( args ): \"\"\" Execute recheck CLI sub-command. Parameters ---------- args : Namespace positional and optional arguments. Returns ------- str The percentage of content currently saved to disk. \"\"\" metafile = args . metafile content = args . content logger . debug ( \"Validating %s against %s contents\" , metafile , content ) checker = Checker ( metafile , content ) logger . debug ( \"Completed initialization of the Checker class\" ) result = checker . results () sys . stdout . write ( str ( result ) + \"% Match \\n \" ) sys . stdout . flush () return result","title":"recheck()"},{"location":"source/#torrentfile.edit","text":"Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"edit"},{"location":"source/#torrentfile.edit--keywords","text":"private comment source trackers web-seeds","title":"Keywords"},{"location":"source/#torrentfile.edit.edit_torrent","text":"Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta","title":"edit_torrent()"},{"location":"source/#torrentfile.edit.filter_empty","text":"Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val )","title":"filter_empty()"},{"location":"source/#torrentfile.hasher","text":"Piece/File Hashers for Bittorrent meta file contents.","title":"hasher"},{"location":"source/#torrentfile.hasher.FileHasher","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Parameters ---------- data : BytesIO File opened in read mode. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"FileHasher"},{"location":"source/#torrentfile.hasher.FileHasher.__init__","text":"Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" )","title":"__init__()"},{"location":"source/#torrentfile.hasher.FileHasher.__iter__","text":"Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.FileHasher.__next__","text":"Calculate layer hashes for contents of file. Parameters: Name Type Description Default data BytesIO File opened in read mode. required Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Parameters ---------- data : BytesIO File opened in read mode. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash","title":"__next__()"},{"location":"source/#torrentfile.hasher.FileHasher._calculate_root","text":"Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.FileHasher._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.Hasher","text":"Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"Hasher"},{"location":"source/#torrentfile.hasher.Hasher.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ]))","title":"__init__()"},{"location":"source/#torrentfile.hasher.Hasher.__iter__","text":"Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.Hasher.__next__","text":"Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"__next__()"},{"location":"source/#torrentfile.hasher.Hasher._handle_partial","text":"Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec","title":"_handle_partial()"},{"location":"source/#torrentfile.hasher.Hasher.next_file","text":"Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False","title":"next_file()"},{"location":"source/#torrentfile.hasher.HasherHybrid","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"HasherHybrid"},{"location":"source/#torrentfile.hasher.HasherHybrid.__init__","text":"Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherHybrid._calculate_root","text":"Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherHybrid._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.HasherHybrid.process_file","text":"Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.HasherV2","text":"Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"HasherV2"},{"location":"source/#torrentfile.hasher.HasherV2.__init__","text":"Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherV2._calculate_root","text":"Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherV2.process_file","text":"Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.merkle_root","text":"Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks","title":"merkle_root()"},{"location":"source/#torrentfile.interactive","text":"Module contains the procedures used for Interactive Mode.","title":"interactive"},{"location":"source/#torrentfile.interactive.InteractiveCreator","text":"Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"InteractiveCreator"},{"location":"source/#torrentfile.interactive.InteractiveCreator.__init__","text":"Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props ()","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveCreator.get_props","text":"Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"get_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor","text":"Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"InteractiveEditor"},{"location":"source/#torrentfile.interactive.InteractiveEditor.__init__","text":"Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), }","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.edit_props","text":"Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"edit_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.sanatize_response","text":"Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val","title":"sanatize_response()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.show_current","text":"Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out )","title":"show_current()"},{"location":"source/#torrentfile.interactive._get_input","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value","title":"_get_input()"},{"location":"source/#torrentfile.interactive._get_input_loop","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" )","title":"_get_input_loop()"},{"location":"source/#torrentfile.interactive.create_torrent","text":"Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator","title":"create_torrent()"},{"location":"source/#torrentfile.interactive.edit_action","text":"Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props ()","title":"edit_action()"},{"location":"source/#torrentfile.interactive.get_input","text":"Determine appropriate input function to call. Parameters: Name Type Description Default args tuple Arbitrary number of args to pass to next function required Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args )","title":"get_input()"},{"location":"source/#torrentfile.interactive.recheck_torrent","text":"Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results","title":"recheck_torrent()"},{"location":"source/#torrentfile.interactive.select_action","text":"Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover","title":"select_action()"},{"location":"source/#torrentfile.interactive.showcenter","text":"Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string )","title":"showcenter()"},{"location":"source/#torrentfile.interactive.showtext","text":"Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt )","title":"showtext()"},{"location":"source/#torrentfile.mixins","text":"Collection of classes that can be used as Mixins with other base classes. Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the progress bar mixin. And any class is eligible to use the callback mixin.","title":"mixins"},{"location":"source/#torrentfile.mixins.CbMixin","text":"Mixin class to set a callback during hashing procedure. Source code in torrentfile\\mixins.py 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class CbMixin : \"\"\" Mixin class to set a callback during hashing procedure. \"\"\" _cb = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover","title":"CbMixin"},{"location":"source/#torrentfile.mixins.CbMixin.set_callback","text":"Assign a callback to the Hashing class. Parameters: Name Type Description Default func function the callback function required Source code in torrentfile\\mixins.py 40 41 42 43 44 45 46 47 48 49 50 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover","title":"set_callback()"},{"location":"source/#torrentfile.mixins.ProgMixin","text":"Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files.","title":"ProgMixin"},{"location":"source/#torrentfile.mixins.ProgMixin--methods","text":"prog_start prog_update prog_close Source code in torrentfile\\mixins.py 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 class ProgMixin : \"\"\" Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods ------- prog_start prog_update prog_close \"\"\" def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start ) def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush () def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False","title":"Methods"},{"location":"source/#torrentfile.mixins.ProgMixin.is_active","text":"Test to see if there is an active progress bar for object. Returns: Name Type Description bool True if there is, otherwise False. Source code in torrentfile\\mixins.py 199 200 201 202 203 204 205 206 207 208 209 210 def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False","title":"is_active()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_close","text":"Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. Source code in torrentfile\\mixins.py 187 188 189 190 191 192 193 194 195 196 197 def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog","title":"prog_close()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_start","text":"Generate a new progress bar for the given file path. Parameters: Name Type Description Default total int the total amount of units accumulating towards. required path str path to file being hashed. required length int the number of characters of the actual progress bar. 50 unit str the text representation of the value being measured. None Source code in torrentfile\\mixins.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start )","title":"prog_start()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_update","text":"Update progress bar with given amount of progress. Parameters: Name Type Description Default val int the number of bytes count the progress bar should increase. required Source code in torrentfile\\mixins.py 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush ()","title":"prog_update()"},{"location":"source/#torrentfile.mixins.ProgressBar","text":"Holds the state and details of the terminal progress bars. Parameters: Name Type Description Default total int the total amount to be accumulated. required title str the subject of the progress tracker required length int the width of the progress bar required unit str the text representation incremented required Source code in torrentfile\\mixins.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 class ProgressBar : \"\"\" Holds the state and details of the terminal progress bars. Parameters ---------- total : int the total amount to be accumulated. title : str the subject of the progress tracker length : int the width of the progress bar unit : str the text representation incremented \"\"\" def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ]) def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar )","title":"ProgressBar"},{"location":"source/#torrentfile.mixins.ProgressBar.__init__","text":"Construct the progress bar object and store state of it's properties. Source code in torrentfile\\mixins.py 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ])","title":"__init__()"},{"location":"source/#torrentfile.mixins.ProgressBar.increment","text":"Increase the state of the progress bar value. Parameters: Name Type Description Default value int the amount to increment the state by. required Source code in torrentfile\\mixins.py 96 97 98 99 100 101 102 103 104 105 def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value","title":"increment()"},{"location":"source/#torrentfile.mixins.ProgressBar.pbar","text":"Return the size of the filled portion of the progress bar. Returns: Name Type Description str the progress bar characters Source code in torrentfile\\mixins.py 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar )","title":"pbar()"},{"location":"source/#torrentfile.mixins.waiting","text":"Show loading message while thread completes processing. Parameters: Name Type Description Default msg str Message string printed before the progress bar required flag list Once flag is filled exit loop required timeout int max amount of time to run the function. 180 Source code in torrentfile\\mixins.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 def waiting ( msg , flag , timeout = 180 ): \"\"\" Show loading message while thread completes processing. Parameters ---------- msg : str Message string printed before the progress bar flag : list Once flag is filled exit loop timeout : int max amount of time to run the function. \"\"\" then = time . time () codes , fill = list ( range ( 9617 , 9620 )), chr ( 9619 ) size = idx = 0 total = shutil . get_terminal_size () . columns - len ( msg ) - 20 def output ( text ): \"\"\" Print parameter message to the console. Parameters ---------- text : str output message \"\"\" sys . stdout . write ( text ) sys . stdout . flush () output ( \" \\n \" ) while not flag : time . sleep ( 0.16 ) filled = ( fill * size ) + chr ( codes [ idx ]) + ( \" \" * ( total - size )) output ( f \" { msg } : { filled } \\r \" ) idx = idx + 1 if idx + 1 < len ( codes ) else 0 size = size + 1 if size < total else 0 if time . time () - then > timeout : break output ( \" \\n \" )","title":"waiting()"},{"location":"source/#torrentfile.recheck","text":"Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole.","title":"recheck"},{"location":"source/#torrentfile.recheck.Checker","text":"Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required","title":"Checker"},{"location":"source/#torrentfile.recheck.Checker--example","text":">> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"Example"},{"location":"source/#torrentfile.recheck.Checker.__init__","text":"Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.Checker.check_paths","text":"Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], [])","title":"check_paths()"},{"location":"source/#torrentfile.recheck.Checker.find_root","text":"Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root )","title":"find_root()"},{"location":"source/#torrentfile.recheck.Checker.iter_hashes","text":"Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"iter_hashes()"},{"location":"source/#torrentfile.recheck.Checker.log_msg","text":"Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default args dict formatting args for log message required level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message )","title":"log_msg()"},{"location":"source/#torrentfile.recheck.Checker.piece_checker","text":"Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker","title":"piece_checker()"},{"location":"source/#torrentfile.recheck.Checker.register_callback","text":"Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook","title":"register_callback()"},{"location":"source/#torrentfile.recheck.Checker.results","text":"Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result","title":"results()"},{"location":"source/#torrentfile.recheck.Checker.walk_file_tree","text":"Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ])","title":"walk_file_tree()"},{"location":"source/#torrentfile.recheck.FeedChecker","text":"Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required hasher Any hashing class for calculating piece hashes. default=None required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. hasher : Any hashing class for calculating piece hashes. default=None \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"FeedChecker"},{"location":"source/#torrentfile.recheck.FeedChecker.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None","title":"__init__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 350 351 352 353 354 355 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__next__","text":"Yield back result of comparison. Source code in torrentfile\\recheck.py 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial )","title":"__next__()"},{"location":"source/#torrentfile.recheck.FeedChecker._gen_padding","text":"Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"_gen_padding()"},{"location":"source/#torrentfile.recheck.FeedChecker.extract","text":"Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad","title":"extract()"},{"location":"source/#torrentfile.recheck.FeedChecker.iter_pieces","text":"Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close ()","title":"iter_pieces()"},{"location":"source/#torrentfile.recheck.HashChecker","text":"Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required hasher Object the version specific hashing class for torrent content. required Source code in torrentfile\\recheck.py 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. hasher : Object the version specific hashing class for torrent content. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"HashChecker"},{"location":"source/#torrentfile.recheck.HashChecker.Padder","text":"Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"Padder"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__init__","text":"Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 534 535 536 537 538 539 540 541 542 543 544 545 546 547 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__iter__","text":"Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 549 550 551 552 553 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__next__","text":"Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.__init__","text":"Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 491 492 493 494 495 496 497 498 499 500 501 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 503 504 505 506 507 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.__next__","text":"Provide the result of comparison. Source code in torrentfile\\recheck.py 509 510 511 512 513 514 515 516 517 518 519 520 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.advance","text":"Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"advance()"},{"location":"source/#torrentfile.recheck.HashChecker.next_file","text":"Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False","title":"next_file()"},{"location":"source/#torrentfile.recheck.HashChecker.process_current","text":"Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err","title":"process_current()"},{"location":"source/#torrentfile.torrent","text":"Classes and procedures pertaining to the creation of torrent meta files.","title":"torrent"},{"location":"source/#torrentfile.torrent--classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#torrentfile.torrent--constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#torrentfile.torrent--bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#torrentfile.torrent--meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#torrentfile.torrent--bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#torrentfile.torrent--v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory.","title":"v1 meta-dictionary"},{"location":"source/#torrentfile.torrent.MetaFile","text":"Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"MetaFile"},{"location":"source/#torrentfile.torrent.MetaFile.__init__","text":"Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name","title":"__init__()"},{"location":"source/#torrentfile.torrent.MetaFile.assemble","text":"Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError","title":"assemble()"},{"location":"source/#torrentfile.torrent.MetaFile.set_callback","text":"Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func )","title":"set_callback()"},{"location":"source/#torrentfile.torrent.MetaFile.sort_meta","text":"Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta","title":"sort_meta()"},{"location":"source/#torrentfile.torrent.MetaFile.write","text":"Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"write()"},{"location":"source/#torrentfile.torrent.TorrentAssembler","text":"Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent options. required Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentAssembler"},{"location":"source/#torrentfile.torrent.TorrentAssembler.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentAssembler._traverse","text":"Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentAssembler.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFile","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default kwargs dict Dictionary containing torrent file options. required Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"TorrentFile"},{"location":"source/#torrentfile.torrent.TorrentFile.__init__","text":"Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default kwargs dict dictionary of keyword args passed to superclass. required Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFile.assemble","text":"Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid","text":"Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent options. required Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentFileHybrid"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid._traverse","text":"Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileV2","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent file options. required Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"TorrentFileV2"},{"location":"source/#torrentfile.torrent.TorrentFileV2.__init__","text":"Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default kwargs dict keywword arguments to pass to superclass. required Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileV2._traverse","text":"Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileV2.assemble","text":"Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers","title":"assemble()"},{"location":"source/#torrentfile.utils","text":"Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field.","title":"utils"},{"location":"source/#torrentfile.utils.Memo","text":"Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"Memo"},{"location":"source/#torrentfile.utils.Memo.__call__","text":"Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"__call__()"},{"location":"source/#torrentfile.utils.Memo.__init__","text":"Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {}","title":"__init__()"},{"location":"source/#torrentfile.utils.MissingPathError","text":"Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"MissingPathError"},{"location":"source/#torrentfile.utils.MissingPathError.__init__","text":"Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils.PieceLengthValueError","text":"Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"PieceLengthValueError"},{"location":"source/#torrentfile.utils.PieceLengthValueError.__init__","text":"Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils._filelist_total","text":"Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist )","title":"_filelist_total()"},{"location":"source/#torrentfile.utils.filelist_total","text":"Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError","title":"filelist_total()"},{"location":"source/#torrentfile.utils.get_file_list","text":"Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist","title":"get_file_list()"},{"location":"source/#torrentfile.utils.get_piece_length","text":"Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp","title":"get_piece_length()"},{"location":"source/#torrentfile.utils.humanize_bytes","text":"Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\"","title":"humanize_bytes()"},{"location":"source/#torrentfile.utils.next_power_2","text":"Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start","title":"next_power_2()"},{"location":"source/#torrentfile.utils.normalize_piece_length","text":"Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError","title":"normalize_piece_length()"},{"location":"source/#torrentfile.utils.path_piece_length","text":"Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize )","title":"path_piece_length()"},{"location":"source/#torrentfile.utils.path_size","text":"Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size","title":"path_size()"},{"location":"source/#torrentfile.utils.path_stat","text":"Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length )","title":"path_stat()"},{"location":"source/#torrentfile.version","text":"Holds the release version number.","title":"version"},{"location":"source/#torrentfile.cli","text":"Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility.","title":"cli"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter","text":"Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"TorrentFileHelpFormatter"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.__init__","text":"Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions )","title":"__init__()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._format_text","text":"Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \"","title":"_format_text()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._join_parts","text":"Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts )","title":"_join_parts()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._split_lines","text":"Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ]","title":"_split_lines()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.format_headers","text":"Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"format_headers()"},{"location":"source/#torrentfile.cli.activate_logger","text":"Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" )","title":"activate_logger()"},{"location":"source/#torrentfile.cli.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args","title":"execute()"},{"location":"source/#torrentfile.cli.main","text":"Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute ()","title":"main()"},{"location":"source/#torrentfile.edit","text":"Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"edit"},{"location":"source/#torrentfile.edit--keywords","text":"private comment source trackers web-seeds","title":"Keywords"},{"location":"source/#torrentfile.edit.edit_torrent","text":"Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta","title":"edit_torrent()"},{"location":"source/#torrentfile.edit.filter_empty","text":"Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val )","title":"filter_empty()"},{"location":"source/#torrentfile.hasher","text":"Piece/File Hashers for Bittorrent meta file contents.","title":"hasher"},{"location":"source/#torrentfile.hasher.FileHasher","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Parameters ---------- data : BytesIO File opened in read mode. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"FileHasher"},{"location":"source/#torrentfile.hasher.FileHasher.__init__","text":"Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" )","title":"__init__()"},{"location":"source/#torrentfile.hasher.FileHasher.__iter__","text":"Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.FileHasher.__next__","text":"Calculate layer hashes for contents of file. Parameters: Name Type Description Default data BytesIO File opened in read mode. required Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Parameters ---------- data : BytesIO File opened in read mode. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash","title":"__next__()"},{"location":"source/#torrentfile.hasher.FileHasher._calculate_root","text":"Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.FileHasher._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.Hasher","text":"Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"Hasher"},{"location":"source/#torrentfile.hasher.Hasher.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ]))","title":"__init__()"},{"location":"source/#torrentfile.hasher.Hasher.__iter__","text":"Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.Hasher.__next__","text":"Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"__next__()"},{"location":"source/#torrentfile.hasher.Hasher._handle_partial","text":"Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec","title":"_handle_partial()"},{"location":"source/#torrentfile.hasher.Hasher.next_file","text":"Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False","title":"next_file()"},{"location":"source/#torrentfile.hasher.HasherHybrid","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"HasherHybrid"},{"location":"source/#torrentfile.hasher.HasherHybrid.__init__","text":"Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherHybrid._calculate_root","text":"Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherHybrid._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.HasherHybrid.process_file","text":"Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.HasherV2","text":"Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"HasherV2"},{"location":"source/#torrentfile.hasher.HasherV2.__init__","text":"Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherV2._calculate_root","text":"Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherV2.process_file","text":"Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.merkle_root","text":"Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks","title":"merkle_root()"},{"location":"source/#torrentfile.interactive","text":"Module contains the procedures used for Interactive Mode.","title":"interactive"},{"location":"source/#torrentfile.interactive.InteractiveCreator","text":"Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"InteractiveCreator"},{"location":"source/#torrentfile.interactive.InteractiveCreator.__init__","text":"Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props ()","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveCreator.get_props","text":"Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"get_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor","text":"Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"InteractiveEditor"},{"location":"source/#torrentfile.interactive.InteractiveEditor.__init__","text":"Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), }","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.edit_props","text":"Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"edit_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.sanatize_response","text":"Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val","title":"sanatize_response()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.show_current","text":"Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out )","title":"show_current()"},{"location":"source/#torrentfile.interactive._get_input","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value","title":"_get_input()"},{"location":"source/#torrentfile.interactive._get_input_loop","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" )","title":"_get_input_loop()"},{"location":"source/#torrentfile.interactive.create_torrent","text":"Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator","title":"create_torrent()"},{"location":"source/#torrentfile.interactive.edit_action","text":"Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props ()","title":"edit_action()"},{"location":"source/#torrentfile.interactive.get_input","text":"Determine appropriate input function to call. Parameters: Name Type Description Default args tuple Arbitrary number of args to pass to next function required Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args )","title":"get_input()"},{"location":"source/#torrentfile.interactive.recheck_torrent","text":"Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results","title":"recheck_torrent()"},{"location":"source/#torrentfile.interactive.select_action","text":"Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover","title":"select_action()"},{"location":"source/#torrentfile.interactive.showcenter","text":"Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string )","title":"showcenter()"},{"location":"source/#torrentfile.interactive.showtext","text":"Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt )","title":"showtext()"},{"location":"source/#torrentfile.recheck","text":"Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole.","title":"recheck"},{"location":"source/#torrentfile.recheck.Checker","text":"Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required","title":"Checker"},{"location":"source/#torrentfile.recheck.Checker--example","text":">> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"Example"},{"location":"source/#torrentfile.recheck.Checker.__init__","text":"Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.Checker.check_paths","text":"Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], [])","title":"check_paths()"},{"location":"source/#torrentfile.recheck.Checker.find_root","text":"Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root )","title":"find_root()"},{"location":"source/#torrentfile.recheck.Checker.iter_hashes","text":"Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"iter_hashes()"},{"location":"source/#torrentfile.recheck.Checker.log_msg","text":"Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default args dict formatting args for log message required level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message )","title":"log_msg()"},{"location":"source/#torrentfile.recheck.Checker.piece_checker","text":"Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker","title":"piece_checker()"},{"location":"source/#torrentfile.recheck.Checker.register_callback","text":"Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook","title":"register_callback()"},{"location":"source/#torrentfile.recheck.Checker.results","text":"Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result","title":"results()"},{"location":"source/#torrentfile.recheck.Checker.walk_file_tree","text":"Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ])","title":"walk_file_tree()"},{"location":"source/#torrentfile.recheck.FeedChecker","text":"Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required hasher Any hashing class for calculating piece hashes. default=None required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. hasher : Any hashing class for calculating piece hashes. default=None \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"FeedChecker"},{"location":"source/#torrentfile.recheck.FeedChecker.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None","title":"__init__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 350 351 352 353 354 355 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__next__","text":"Yield back result of comparison. Source code in torrentfile\\recheck.py 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial )","title":"__next__()"},{"location":"source/#torrentfile.recheck.FeedChecker._gen_padding","text":"Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"_gen_padding()"},{"location":"source/#torrentfile.recheck.FeedChecker.extract","text":"Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad","title":"extract()"},{"location":"source/#torrentfile.recheck.FeedChecker.iter_pieces","text":"Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close ()","title":"iter_pieces()"},{"location":"source/#torrentfile.recheck.HashChecker","text":"Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required hasher Object the version specific hashing class for torrent content. required Source code in torrentfile\\recheck.py 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. hasher : Object the version specific hashing class for torrent content. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"HashChecker"},{"location":"source/#torrentfile.recheck.HashChecker.Padder","text":"Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"Padder"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__init__","text":"Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 534 535 536 537 538 539 540 541 542 543 544 545 546 547 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__iter__","text":"Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 549 550 551 552 553 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__next__","text":"Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.__init__","text":"Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 491 492 493 494 495 496 497 498 499 500 501 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 503 504 505 506 507 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.__next__","text":"Provide the result of comparison. Source code in torrentfile\\recheck.py 509 510 511 512 513 514 515 516 517 518 519 520 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.advance","text":"Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"advance()"},{"location":"source/#torrentfile.recheck.HashChecker.next_file","text":"Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False","title":"next_file()"},{"location":"source/#torrentfile.recheck.HashChecker.process_current","text":"Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err","title":"process_current()"},{"location":"source/#torrentfile.torrent","text":"Classes and procedures pertaining to the creation of torrent meta files.","title":"torrent"},{"location":"source/#torrentfile.torrent--classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#torrentfile.torrent--constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#torrentfile.torrent--bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#torrentfile.torrent--meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#torrentfile.torrent--bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#torrentfile.torrent--v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory.","title":"v1 meta-dictionary"},{"location":"source/#torrentfile.torrent.MetaFile","text":"Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"MetaFile"},{"location":"source/#torrentfile.torrent.MetaFile.__init__","text":"Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name","title":"__init__()"},{"location":"source/#torrentfile.torrent.MetaFile.assemble","text":"Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError","title":"assemble()"},{"location":"source/#torrentfile.torrent.MetaFile.set_callback","text":"Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func )","title":"set_callback()"},{"location":"source/#torrentfile.torrent.MetaFile.sort_meta","text":"Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta","title":"sort_meta()"},{"location":"source/#torrentfile.torrent.MetaFile.write","text":"Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"write()"},{"location":"source/#torrentfile.torrent.TorrentAssembler","text":"Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent options. required Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentAssembler"},{"location":"source/#torrentfile.torrent.TorrentAssembler.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentAssembler._traverse","text":"Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentAssembler.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFile","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default kwargs dict Dictionary containing torrent file options. required Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"TorrentFile"},{"location":"source/#torrentfile.torrent.TorrentFile.__init__","text":"Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default kwargs dict dictionary of keyword args passed to superclass. required Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFile.assemble","text":"Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid","text":"Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent options. required Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentFileHybrid"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid._traverse","text":"Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileV2","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default kwargs dict Keyword arguments for torrent file options. required Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"TorrentFileV2"},{"location":"source/#torrentfile.torrent.TorrentFileV2.__init__","text":"Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default kwargs dict keywword arguments to pass to superclass. required Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileV2._traverse","text":"Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileV2.assemble","text":"Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers","title":"assemble()"},{"location":"source/#torrentfile.utils","text":"Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field.","title":"utils"},{"location":"source/#torrentfile.utils.Memo","text":"Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"Memo"},{"location":"source/#torrentfile.utils.Memo.__call__","text":"Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"__call__()"},{"location":"source/#torrentfile.utils.Memo.__init__","text":"Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {}","title":"__init__()"},{"location":"source/#torrentfile.utils.MissingPathError","text":"Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"MissingPathError"},{"location":"source/#torrentfile.utils.MissingPathError.__init__","text":"Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils.PieceLengthValueError","text":"Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"PieceLengthValueError"},{"location":"source/#torrentfile.utils.PieceLengthValueError.__init__","text":"Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils._filelist_total","text":"Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist )","title":"_filelist_total()"},{"location":"source/#torrentfile.utils.filelist_total","text":"Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError","title":"filelist_total()"},{"location":"source/#torrentfile.utils.get_file_list","text":"Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist","title":"get_file_list()"},{"location":"source/#torrentfile.utils.get_piece_length","text":"Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp","title":"get_piece_length()"},{"location":"source/#torrentfile.utils.humanize_bytes","text":"Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\"","title":"humanize_bytes()"},{"location":"source/#torrentfile.utils.next_power_2","text":"Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start","title":"next_power_2()"},{"location":"source/#torrentfile.utils.normalize_piece_length","text":"Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError","title":"normalize_piece_length()"},{"location":"source/#torrentfile.utils.path_piece_length","text":"Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize )","title":"path_piece_length()"},{"location":"source/#torrentfile.utils.path_size","text":"Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size","title":"path_size()"},{"location":"source/#torrentfile.utils.path_stat","text":"Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length )","title":"path_stat()"},{"location":"source/#torrentfile.version","text":"Holds the release version number.","title":"version"},{"location":"source/#tests","text":"Unittest package init module.","title":"tests"},{"location":"source/#tests.dir1","text":"Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 168 169 170 171 172 173 174 175 176 177 178 179 @pytest . fixture ( scope = \"package\" ) def dir1 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir () yield root rmpath ( root )","title":"dir1()"},{"location":"source/#tests.dir2","text":"Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 182 183 184 185 186 187 188 189 190 191 192 193 @pytest . fixture () def dir2 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir ( ext = \"2\" ) yield Path ( root ) rmpath ( root )","title":"dir2()"},{"location":"source/#tests.file1","text":"Return the path to a temporary file package scope. Source code in tests\\__init__.py 241 242 243 244 245 246 247 248 @pytest . fixture ( scope = \"package\" ) def file1 (): \"\"\" Return the path to a temporary file package scope. \"\"\" path = tempfile () yield path rmpath ( path )","title":"file1()"},{"location":"source/#tests.file2","text":"Return the path to a temporary file no scope. Source code in tests\\__init__.py 297 298 299 300 301 302 303 304 @pytest . fixture () def file2 (): \"\"\" Return the path to a temporary file no scope. \"\"\" path = tempfile () yield path rmpath ( path )","title":"file2()"},{"location":"source/#tests.filemeta1","text":"Test fixture for generating metafile for all versions of torrents. Source code in tests\\__init__.py 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 @pytest . fixture ( scope = \"package\" , params = torrents ()) def filemeta1 ( file1 , request ): \"\"\" Test fixture for generating metafile for all versions of torrents. \"\"\" args = { \"path\" : file1 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file1 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile )","title":"filemeta1()"},{"location":"source/#tests.filemeta2","text":"Test fixture for generating a meta file no scope. Source code in tests\\__init__.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 @pytest . fixture ( params = torrents ()) def filemeta2 ( file2 , request ): \"\"\" Test fixture for generating a meta file no scope. \"\"\" args = { \"path\" : file2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url7\" , \"url8\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file2 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile )","title":"filemeta2()"},{"location":"source/#tests.metafile1","text":"Create a standard metafile for testing. Source code in tests\\__init__.py 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 @pytest . fixture ( scope = \"package\" , params = torrents ()) def metafile1 ( dir1 , request ): \"\"\" Create a standard metafile for testing. \"\"\" versions = torrents () args = { \"path\" : dir1 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir1 ) + str ( versions . index ( torrent_class )) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile )","title":"metafile1()"},{"location":"source/#tests.metafile2","text":"Create a standard metafile for testing. Source code in tests\\__init__.py 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 @pytest . fixture ( params = torrents ()) def metafile2 ( dir2 , request ): \"\"\" Create a standard metafile for testing. \"\"\" args = { \"path\" : dir2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"httpseeds\" : [ \"url6\" , \"url7\" ], \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir2 ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile )","title":"metafile2()"},{"location":"source/#tests.rmpath","text":"Remove file or directory path. Parameters: Name Type Description Default args list Filesystem locations for removing. required Source code in tests\\__init__.py 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def rmpath ( * args ): \"\"\"Remove file or directory path. Parameters ---------- args : list Filesystem locations for removing. \"\"\" for arg in args : if not os . path . exists ( arg ): continue if os . path . isdir ( arg ): try : shutil . rmtree ( arg ) except PermissionError : # pragma: nocover pass elif os . path . isfile ( arg ): try : os . remove ( arg ) except PermissionError : # pragma: nocover pass","title":"rmpath()"},{"location":"source/#tests.sizedfiles","text":"Generate variable sized meta files for testing, no scope. Source code in tests\\__init__.py 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 @pytest . fixture ( params = torrents ()) def sizedfiles ( dir2 , sizes , request ): \"\"\" Generate variable sized meta files for testing, no scope. \"\"\" versions = torrents () args = { \"content\" : dir2 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , \"piece_length\" : sizes , } torrent_class = request . param version = str ( versions . index ( torrent_class )) outfile = str ( dir2 ) + version + str ( sizes ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile )","title":"sizedfiles()"},{"location":"source/#tests.sizes","text":"Generate powers of 2 for file creation package scope. Source code in tests\\__init__.py 159 160 161 162 163 164 165 @pytest . fixture ( scope = \"package\" , params = [ 2 ** i for i in range ( 15 , 20 )]) def sizes ( request ): \"\"\" Generate powers of 2 for file creation package scope. \"\"\" size = request . param yield size","title":"sizes()"},{"location":"source/#tests.teardown","text":"Remove all temporary directories and files. Source code in tests\\__init__.py 141 142 143 144 145 146 147 148 149 @atexit . register def teardown (): # pragma: nocover \"\"\" Remove all temporary directories and files. \"\"\" root = Path ( __file__ ) . parent / \"TESTDIR\" for path in [ root , \"./torrentfile.log\" ]: if os . path . exists ( path ): rmpath ( path )","title":"teardown()"},{"location":"source/#tests.tempdir","text":"Create temporary directory. Parameters: Name Type Description Default ext str , optional extension to file names, by default \"1\" '1' Returns: Type Description str path to common root for directory. Source code in tests\\__init__.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 def tempdir ( ext = \"1\" ): \"\"\"Create temporary directory. Parameters ---------- ext : str, optional extension to file names, by default \"1\" Returns ------- str path to common root for directory. \"\"\" layouts = { \"1\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.mp4\" , f \"dir { ext } /file3.mp3\" , f \"dir { ext } /file4.zip\" , f \"dir { ext } /file5.txt\" , f \"dir { ext } /subdir1/subdir2/file.7z\" , f \"dir { ext } /subdir/subdir/file4.rar\" , f \"dir { ext } /subdir/subdir/file4.r01\" , ], \"2\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.jpg\" , f \"dir { ext } /subdir/file2.mp4\" , f \"dir { ext } /subdir/file3.mp3\" , ], } paths = [] for path in layouts [ ext ]: temps = tempfile ( path = path , exp = 18 ) paths . append ( temps ) return os . path . commonpath ( paths )","title":"tempdir()"},{"location":"source/#tests.tempfile","text":"Create temporary file. Creates a temporary file for unittesting purposes.py Parameters: Name Type Description Default path str , optional relative path to temporary files, by default None None exp int , optional Exponent used to determine size of file., by default 18 18 Returns: Type Description str absolute path to file. Source code in tests\\__init__.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 def tempfile ( path = None , exp = 18 ): \"\"\"Create temporary file. Creates a temporary file for unittesting purposes.py Parameters ---------- path : str, optional relative path to temporary files, by default None exp : int, optional Exponent used to determine size of file., by default 18 Returns ------- str absolute path to file. \"\"\" seq = ( string . printable + string . whitespace ) . encode ( \"utf-8\" ) root = Path ( __file__ ) . parent / \"TESTDIR\" if not os . path . exists ( root ): os . mkdir ( root ) if not path : path = root / ( str ( datetime . timestamp ( datetime . now ())) + \".file\" ) parts = Path ( path ) . parts partial = root for i , part in enumerate ( parts ): partial = partial / part if i == len ( parts ) - 1 : with open ( partial , \"wb\" ) as binfile : size = 2 ** exp while size > 0 : if len ( seq ) < size : binfile . write ( seq ) size -= len ( seq ) seq += seq else : binfile . write ( seq [: size ]) size -= size else : if not os . path . exists ( partial ): os . mkdir ( partial ) return partial","title":"tempfile()"},{"location":"source/#tests.torrents","text":"Return seq of torrentfile objects. Source code in tests\\__init__.py 152 153 154 155 156 def torrents (): \"\"\" Return seq of torrentfile objects. \"\"\" return [ TorrentFile , TorrentFileV2 , TorrentFileHybrid , TorrentAssembler ]","title":"torrents()"},{"location":"source/#tests.test_cli","text":"Testing functions for the command line interface.","title":"test_cli"},{"location":"source/#tests.test_cli.folder","text":"Yield a folder object as fixture. Source code in tests\\test_cli.py 41 42 43 44 45 46 47 48 49 @pytest . fixture ( scope = \"module\" ) def folder ( dir1 ): \"\"\" Yield a folder object as fixture. \"\"\" sfolder = str ( dir1 ) torrent = sfolder + \".torrent\" yield ( sfolder , torrent ) rmpath ( torrent )","title":"folder()"},{"location":"source/#tests.test_cli.test_cli_announce","text":"Test announce cli flag. Source code in tests\\test_cli.py 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce ( folder , piece_length , version ): \"\"\" Test announce cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--tracker\" , \"https://announce.org/tracker\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"announce\" ] == \"https://announce.org/tracker\"","title":"test_cli_announce()"},{"location":"source/#tests.test_cli.test_cli_announce_list","text":"Test announce-list cli flag. Source code in tests\\test_cli.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce_list ( folder , version ): \"\"\" Test announce-list cli flag. \"\"\" folder , torrent = folder trackers = [ \"https://announce.org/tracker\" , \"https://announce.net/tracker\" , \"https://tracker.net/announce\" , ] args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , version , \"--tracker\" , ] + trackers sys . argv = args execute () meta = pyben . load ( torrent ) for url in trackers : assert url in [ j for i in meta [ \"announce-list\" ] for j in i ]","title":"test_cli_announce_list()"},{"location":"source/#tests.test_cli.test_cli_announce_path","text":"Test CLI when path is placed after the trackers flag. Source code in tests\\test_cli.py 451 452 453 454 455 456 457 458 459 460 461 462 463 @pytest . mark . parametrize ( \"flag\" , [ \"-t\" , \"-w\" , \"--announce\" , \"--web-seed\" , \"--http-seed\" ] ) def test_cli_announce_path ( dir1 , flag ): \"\"\" Test CLI when path is placed after the trackers flag. \"\"\" args = [ \"torrentfile\" , \"create\" , flag , \"https://announce1.org\" , str ( dir1 )] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_announce_path()"},{"location":"source/#tests.test_cli.test_cli_comment","text":"Test comment cli flag. Source code in tests\\test_cli.py 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_comment ( folder , piece_length , version ): \"\"\" Test comment cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--magnet\" , \"--comment\" , \"this is a comment\" , \"--progress\" , \"1\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"comment\" ] == \"this is a comment\"","title":"test_cli_comment()"},{"location":"source/#tests.test_cli.test_cli_created_by","text":"Test if created torrents recieve a created by field in meta info. Source code in tests\\test_cli.py 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_created_by ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a created by field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"TorrentFile\" in meta [ \"created by\" ]","title":"test_cli_created_by()"},{"location":"source/#tests.test_cli.test_cli_creation_date","text":"Test if torrents created get an accurate timestamp. Source code in tests\\test_cli.py 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_creation_date ( folder , piece_length , version ): \"\"\" Test if torrents created get an accurate timestamp. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) num = float ( meta [ \"creation date\" ]) date = datetime . datetime . fromtimestamp ( num ) now = datetime . datetime . now () assert date . day == now . day assert date . year == now . year assert date . month == now . month","title":"test_cli_creation_date()"},{"location":"source/#tests.test_cli.test_cli_cwd","text":"Test outfile cli flag. Source code in tests\\test_cli.py 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 def test_cli_cwd ( folder ): \"\"\" Test outfile cli flag. \"\"\" folder , _ = folder args = [ \"torrentfile\" , \"create\" , \"--cwd\" , folder , ] sys . argv = args current = os . getcwd () name = os . path . basename ( folder ) outfile = os . path . join ( current , name ) + \".torrent\" execute () assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_cwd()"},{"location":"source/#tests.test_cli.test_cli_empty_files","text":"Test creating torrent with empty files. Source code in tests\\test_cli.py 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"progress\" , [ \"0\" , \"1\" ]) def test_cli_empty_files ( dir2 , version , progress ): \"\"\" Test creating torrent with empty files. \"\"\" args = [ \"torrentfile\" , \"create\" , str ( dir2 ), \"--meta-version\" , version , \"--source\" , \"somesource\" , \"--prog\" , progress , ] sys . argv = args def walk ( root , count ): \"\"\" Traverse directory to edit files. \"\"\" if root . is_file (): with open ( root , \"wb\" ) as _ : return 1 elif root . is_dir (): for item in root . iterdir (): if count >= 2 : break count += walk ( item , count ) return count walk ( dir2 , 0 ) execute () outfile = str ( dir2 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_empty_files()"},{"location":"source/#tests.test_cli.test_cli_help","text":"Test showing help notice cli flag. Source code in tests\\test_cli.py 352 353 354 355 356 357 358 359 360 361 def test_cli_help (): \"\"\" Test showing help notice cli flag. \"\"\" args = [ \"-h\" ] sys . argv = args try : assert execute () except SystemExit : assert True","title":"test_cli_help()"},{"location":"source/#tests.test_cli.test_cli_outfile","text":"Test outfile cli flag. Source code in tests\\test_cli.py 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_outfile ( dir1 , piece_length , version ): \"\"\" Test outfile cli flag. \"\"\" outfile = dir1 + \"test.torrent\" args = [ \"torrentfile\" , \"create\" , dir1 , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-o\" , outfile , \"--prog\" , \"1\" , ] sys . argv = args execute () assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_outfile()"},{"location":"source/#tests.test_cli.test_cli_piece_length","text":"Test piece length cli flag. Source code in tests\\test_cli.py 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_piece_length ( folder , piece_length , version ): \"\"\" Test piece length cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--progress\" , \"0\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"piece length\" ] == piece_length","title":"test_cli_piece_length()"},{"location":"source/#tests.test_cli.test_cli_private","text":"Test private cli flag. Source code in tests\\test_cli.py 85 86 87 88 89 90 91 92 93 94 def test_cli_private ( folder ): \"\"\" Test private cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--private\" ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"private\" in meta [ \"info\" ]","title":"test_cli_private()"},{"location":"source/#tests.test_cli.test_cli_slash_outpath","text":"Test if output when outpath ends with a /. Source code in tests\\test_cli.py 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 @pytest . mark . parametrize ( \"sep\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_outpath ( dir1 , sep ): \"\"\" Test if output when outpath ends with a /. \"\"\" if sys . platform != \"win32\" : sep = \"/\" # pragma: nocover parent = os . path . dirname ( dir1 ) + sep args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , \"-o\" , parent , str ( dir1 ), ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_slash_outpath()"},{"location":"source/#tests.test_cli.test_cli_slash_path","text":"Test if output when path ends with a /. Source code in tests\\test_cli.py 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 @pytest . mark . parametrize ( \"ending\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_path ( dir1 , ending ): \"\"\" Test if output when path ends with a /. \"\"\" if sys . platform != \"win32\" and ending == \" \\\\ \" : # pragma: nocover ending = \"/\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , str ( dir1 ) + ending , ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_slash_path()"},{"location":"source/#tests.test_cli.test_cli_v1","text":"Basic create torrent cli command. Source code in tests\\test_cli.py 52 53 54 55 56 57 58 59 60 def test_cli_v1 ( folder ): \"\"\" Basic create torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_v1()"},{"location":"source/#tests.test_cli.test_cli_v2","text":"Create torrent v2 cli command. Source code in tests\\test_cli.py 63 64 65 66 67 68 69 70 71 def test_cli_v2 ( folder ): \"\"\" Create torrent v2 cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"2\" ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_v2()"},{"location":"source/#tests.test_cli.test_cli_v3","text":"Create hybrid torrent cli command. Source code in tests\\test_cli.py 74 75 76 77 78 79 80 81 82 def test_cli_v3 ( folder ): \"\"\" Create hybrid torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"3\" ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_v3()"},{"location":"source/#tests.test_cli.test_cli_web_seeds","text":"Test if created torrents recieve a web seeds field in meta info. Source code in tests\\test_cli.py 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_web_seeds ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a web seeds field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-w\" , \"https://webseed.url/1\" , \"https://webseed.url/2\" , \"https://webseed.url/3\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"https://webseed.url/1\" in meta [ \"url-list\" ]","title":"test_cli_web_seeds()"},{"location":"source/#tests.test_cli.test_cli_with_debug","text":"Test debug mode cli flag. Source code in tests\\test_cli.py 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_debug ( folder , piece_length , version ): \"\"\" Test debug mode cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_with_debug()"},{"location":"source/#tests.test_cli.test_cli_with_source","text":"Test source cli flag. Source code in tests\\test_cli.py 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_source ( folder , piece_length , version ): \"\"\" Test source cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--source\" , \"somesource\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"source\" ] == \"somesource\"","title":"test_cli_with_source()"},{"location":"source/#tests.test_cli.test_fix","text":"Test dir1 fixture is not None. Source code in tests\\test_cli.py 34 35 36 37 38 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and dir2","title":"test_fix()"},{"location":"source/#tests.test_commands","text":"Testing functions for the sub-action commands from command line args.","title":"test_commands"},{"location":"source/#tests.test_commands.test_create_unicode_name","text":"Test Unicode information in CLI args. Source code in tests\\test_commands.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 def test_create_unicode_name ( file1 ): \"\"\" Test Unicode information in CLI args. \"\"\" parent = os . path . dirname ( file1 ) filename = os . path . join ( parent , \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" ) args = [ \"torrentfile\" , \"-v\" , \"create\" , \"-a\" , \"tracker_url.com/announce_3456\" , \"tracker_url.net/announce_3456\" , \"--source\" , \"sourcetext\" , \"--comment\" , \"filename is \u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"-o\" , str ( filename ), str ( file1 ), ] sys . argv = args execute () assert os . path . exists ( filename )","title":"test_create_unicode_name()"},{"location":"source/#tests.test_commands.test_fix","text":"Test dir1 fixture is not None. Source code in tests\\test_commands.py 37 38 39 40 41 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and metafile1 and file1 and metafile2 and dir2","title":"test_fix()"},{"location":"source/#tests.test_commands.test_info","text":"Test the info_command action from the Command Line Interface. Source code in tests\\test_commands.py 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 @pytest . mark . parametrize ( \"field\" , [ \"name\" , \"announce\" , \"source\" , \"comment\" , \"private\" , \"announce-list\" ], ) def test_info ( field , file1 ): \"\"\" Test the info_command action from the Command Line Interface. \"\"\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"url1\" , \"url2\" , \"url3\" , \"--web-seed\" , \"url4\" , \"url5\" , \"--http-seed\" , \"url6\" , \"url7\" , \"--private\" , \"--comment\" , \"ExampleComment\" , \"--source\" , \"examplesource\" , str ( file1 ), ] sys . argv = args execute () class Space : \"\"\" Stand in substitution for argparse.Namespace object. \"\"\" metafile = str ( file1 ) + \".torrent\" output = info ( Space ) assert field in output","title":"test_info()"},{"location":"source/#tests.test_commands.test_magnet","text":"Test create magnet function scheme. Source code in tests\\test_commands.py 65 66 67 68 69 70 def test_magnet ( metafile1 ): \"\"\" Test create magnet function scheme. \"\"\" magnet_link = magnet ( metafile1 ) assert magnet_link . startswith ( \"magnet\" )","title":"test_magnet()"},{"location":"source/#tests.test_commands.test_magnet_cli","text":"Test magnet creation through CLI interface. Source code in tests\\test_commands.py 136 137 138 139 140 141 142 def test_magnet_cli ( metafile1 ): \"\"\" Test magnet creation through CLI interface. \"\"\" sys . argv [ 1 :] = [ \"m\" , str ( metafile1 )] uri = execute () assert \"magnet\" in uri","title":"test_magnet_cli()"},{"location":"source/#tests.test_commands.test_magnet_empty","text":"Test create magnet function scheme. Source code in tests\\test_commands.py 84 85 86 87 88 89 90 91 def test_magnet_empty (): \"\"\" Test create magnet function scheme. \"\"\" try : magnet ( \"file_that_does_not_exist\" ) except FileNotFoundError : assert True","title":"test_magnet_empty()"},{"location":"source/#tests.test_commands.test_magnet_hex","text":"Test create magnet function digest. Source code in tests\\test_commands.py 54 55 56 57 58 59 60 61 62 def test_magnet_hex ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) info = meta [ \"info\" ] binfo = sha1 ( pyben . dumps ( info )) . hexdigest () . upper () assert binfo in magnet_link","title":"test_magnet_hex()"},{"location":"source/#tests.test_commands.test_magnet_no_announce_list","text":"Test create magnet function scheme. Source code in tests\\test_commands.py 73 74 75 76 77 78 79 80 81 def test_magnet_no_announce_list ( metafile2 ): \"\"\" Test create magnet function scheme. \"\"\" meta = pyben . load ( metafile2 ) del meta [ \"announce-list\" ] pyben . dump ( meta , metafile2 ) magnet_link = magnet ( metafile2 ) assert magnet_link . startswith ( \"magnet\" )","title":"test_magnet_no_announce_list()"},{"location":"source/#tests.test_commands.test_magnet_uri","text":"Test create magnet function digest. Source code in tests\\test_commands.py 44 45 46 47 48 49 50 51 def test_magnet_uri ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) announce = meta [ \"announce\" ] assert quote_plus ( announce ) in magnet_link","title":"test_magnet_uri()"},{"location":"source/#tests.test_commands.test_merkle_root_no_blocks","text":"Test running merkle root function with 1 and 0 len lists. Source code in tests\\test_commands.py 171 172 173 174 175 176 177 178 179 @pytest . mark . parametrize ( \"blocks\" , [[], [ sha1 ( b \"1010\" ) . digest ()]]) # nosec def test_merkle_root_no_blocks ( blocks ): \"\"\" Test running merkle root function with 1 and 0 len lists. \"\"\" if blocks : assert merkle_root ( blocks ) else : assert not merkle_root ( blocks )","title":"test_merkle_root_no_blocks()"},{"location":"source/#tests.test_commands.test_mixins_progbar","text":"Test progbar mixins with small file. Source code in tests\\test_commands.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 @pytest . mark . parametrize ( \"torrent\" , torrents ()) def test_mixins_progbar ( torrent ): \"\"\" Test progbar mixins with small file. \"\"\" tfile = tempfile ( exp = 14 ) msg = \"1234abcd\" * 80 with open ( tfile , \"wb\" ) as temp : temp . write ( msg . encode ( \"utf-8\" )) args = { \"path\" : tfile , \"--prog\" : \"1\" , } metafile = torrent ( ** args ) output , _ = metafile . write () assert output == str ( tfile ) + \".torrent\" rmpath ( tfile )","title":"test_mixins_progbar()"},{"location":"source/#tests.test_edit","text":"Testing the edit torrent feature.","title":"test_edit"},{"location":"source/#tests.test_edit.test_edit_cli","text":"Test edit torrent with all params on cli. Source code in tests\\test_edit.py 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 @pytest . mark . parametrize ( \"comment\" , [ \"commenta\" , \"commentb\" , \"commentc\" ]) @pytest . mark . parametrize ( \"source\" , [ \"sourcea\" , \"sourceb\" , \"sourcec\" ]) @pytest . mark . parametrize ( \"announce\" , [[ \"url1\" , \"url2\" , \"url3\" ], [ \"url1\" ]]) @pytest . mark . parametrize ( \"webseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) @pytest . mark . parametrize ( \"httpseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) def test_edit_cli ( metafile2 , comment , source , announce , webseed , httpseed ): \"\"\" Test edit torrent with all params on cli. \"\"\" sys . argv = [ \"torrentfile\" , \"edit\" , metafile2 , \"--comment\" , comment , \"--source\" , source , \"--web-seed\" , webseed , \"--http-seed\" , httpseed , \"--tracker\" , announce , \"--private\" , ] main () meta = pyben . load ( metafile2 ) info = meta [ \"info\" ] assert comment == info . get ( \"comment\" ) assert source == info . get ( \"source\" ) assert info . get ( \"private\" ) == 1 assert meta [ \"announce-list\" ] == [[ announce ]] assert meta [ \"url-list\" ] == [ webseed ]","title":"test_edit_cli()"},{"location":"source/#tests.test_edit.test_edit_comment","text":"Test edit torrent with comment param. Source code in tests\\test_edit.py 118 119 120 121 122 123 124 125 126 127 @pytest . mark . parametrize ( \"comment\" , [ \"COMMENT\" , \"COMIT\" , \"MITCO\" ]) def test_edit_comment ( metafile2 , comment ): \"\"\" Test edit torrent with comment param. \"\"\" edits = { \"comment\" : comment } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"comment\" ] == comment","title":"test_edit_comment()"},{"location":"source/#tests.test_edit.test_edit_httpseeds","text":"Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 104 105 106 107 108 109 110 111 112 113 114 115 @pytest . mark . parametrize ( \"httpseed\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_httpseeds ( metafile2 , httpseed ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"httpseeds\" : httpseed } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseed","title":"test_edit_httpseeds()"},{"location":"source/#tests.test_edit.test_edit_httpseeds_str","text":"Test edit torrent with webseed param. Source code in tests\\test_edit.py 78 79 80 81 82 83 84 85 86 87 @pytest . mark . parametrize ( \"httpseeds\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_httpseeds_str ( metafile2 , httpseeds ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"httpseeds\" : httpseeds } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseeds . split ()","title":"test_edit_httpseeds_str()"},{"location":"source/#tests.test_edit.test_edit_none","text":"Test edit torrent with None for all params. Source code in tests\\test_edit.py 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 def test_edit_none ( metafile2 ): \"\"\" Test edit torrent with None for all params. \"\"\" edits = { \"announce\" : None , \"url-list\" : None , \"comment\" : None , \"source\" : None , \"private\" : None , } data = pyben . load ( metafile2 ) edited = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta == edited","title":"test_edit_none()"},{"location":"source/#tests.test_edit.test_edit_private_false","text":"Test edit torrent with private param False. Source code in tests\\test_edit.py 153 154 155 156 157 158 159 160 161 def test_edit_private_false ( metafile2 ): \"\"\" Test edit torrent with private param False. \"\"\" edits = { \"private\" : \"\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert \"private\" not in data [ \"info\" ]","title":"test_edit_private_false()"},{"location":"source/#tests.test_edit.test_edit_private_true","text":"Test edit torrent with private param. Source code in tests\\test_edit.py 142 143 144 145 146 147 148 149 150 def test_edit_private_true ( metafile2 ): \"\"\" Test edit torrent with private param. \"\"\" edits = { \"private\" : \"1\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"private\" ] == 1","title":"test_edit_private_true()"},{"location":"source/#tests.test_edit.test_edit_removal","text":"Test edit torrent with empty for all params. Source code in tests\\test_edit.py 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_edit_removal ( metafile2 ): \"\"\" Test edit torrent with empty for all params. \"\"\" edits = { \"announce\" : \"\" , \"url-list\" : \"\" , \"httpseeds\" : \"\" , \"comment\" : \"\" , \"source\" : \"\" , \"private\" : \"\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta","title":"test_edit_removal()"},{"location":"source/#tests.test_edit.test_edit_source","text":"Test edit torrent with source param. Source code in tests\\test_edit.py 130 131 132 133 134 135 136 137 138 139 @pytest . mark . parametrize ( \"source\" , [ \"SomeSource\" , \"NoSouce\" , \"MidSource\" ]) def test_edit_source ( metafile2 , source ): \"\"\" Test edit torrent with source param. \"\"\" edits = { \"source\" : source } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"source\" ] == source","title":"test_edit_source()"},{"location":"source/#tests.test_edit.test_edit_torrent","text":"Test edit torrent with announce param. Source code in tests\\test_edit.py 40 41 42 43 44 45 46 47 48 49 50 51 @pytest . mark . parametrize ( \"announce\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_torrent ( metafile2 , announce ): \"\"\" Test edit torrent with announce param. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce ]","title":"test_edit_torrent()"},{"location":"source/#tests.test_edit.test_edit_torrent_str","text":"Test edit torrent with announce param as string. Source code in tests\\test_edit.py 54 55 56 57 58 59 60 61 62 63 @pytest . mark . parametrize ( \"announce\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_torrent_str ( metafile2 , announce ): \"\"\" Test edit torrent with announce param as string. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce . split ()]","title":"test_edit_torrent_str()"},{"location":"source/#tests.test_edit.test_edit_urllist","text":"Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 90 91 92 93 94 95 96 97 98 99 100 101 @pytest . mark . parametrize ( \"url_list\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_urllist ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list","title":"test_edit_urllist()"},{"location":"source/#tests.test_edit.test_edit_urllist_str","text":"Test edit torrent with webseed param. Source code in tests\\test_edit.py 66 67 68 69 70 71 72 73 74 75 @pytest . mark . parametrize ( \"url_list\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_urllist_str ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list . split ()","title":"test_edit_urllist_str()"},{"location":"source/#tests.test_edit.test_fix","text":"Testing dir fixtures. Source code in tests\\test_edit.py 33 34 35 36 37 def test_fix (): \"\"\" Testing dir fixtures. \"\"\" assert dir2 and metafile2 and dir1","title":"test_fix()"},{"location":"source/#tests.test_edit.test_metafile_edit_with_unicode","text":"Test if editing full unicode works as it should. Source code in tests\\test_edit.py 233 234 235 236 237 238 239 240 241 242 243 244 245 246 def test_metafile_edit_with_unicode ( metafile2 ): \"\"\" Test if editing full unicode works as it should. \"\"\" edits = { \"comment\" : \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"source\" : \"\u4e02\u4e03\u4e07\u4e0f\u4e11\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) com1 = data [ \"info\" ][ \"comment\" ] com2 = meta [ \"info\" ][ \"comment\" ] msg = edits [ \"comment\" ] assert com1 == com2 == msg","title":"test_metafile_edit_with_unicode()"},{"location":"source/#tests.test_interactive","text":"Testing functions for the command line interface.","title":"test_interactive"},{"location":"source/#tests.test_interactive.test_fixtures","text":"Test the fixtures used in module. Source code in tests\\test_interactive.py 37 38 39 40 41 def test_fixtures (): \"\"\" Test the fixtures used in module. \"\"\" assert filemeta2 and file1 and file2","title":"test_fixtures()"},{"location":"source/#tests.test_interactive.test_inter_create_full","text":"Test creating torrent interactively with many parameters. Source code in tests\\test_interactive.py 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"piece_length\" , [ \"23\" , \"18\" , \"131072\" ]) @pytest . mark . parametrize ( \"announce\" , [ \"url1\" , \"urla urlb urlc\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Do\" , \"Ra\" , \"Me\" ]) def test_inter_create_full ( file1 , piece_length , announce , comment , source , url_list , version , monkeypatch , ): \"\"\" Test creating torrent interactively with many parameters. \"\"\" mapping = [ \"create\" , piece_length , announce , url_list , url_list , comment , source , \"Y\" , file1 , str ( file1 ) + \".torrent\" , version , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta = pyben . load ( str ( file1 ) + \".torrent\" ) assert meta [ \"info\" ][ \"source\" ] == source assert meta [ \"info\" ][ \"piece length\" ] == normalize_piece_length ( piece_length ) assert meta [ \"info\" ][ \"comment\" ] == comment assert meta [ \"url-list\" ] == url_list . split ()","title":"test_inter_create_full()"},{"location":"source/#tests.test_interactive.test_inter_edit_cli","text":"Test editing torrent interactively from CLI. Source code in tests\\test_interactive.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 @pytest . mark . parametrize ( \"announce\" , [ \"urla urlb urlc\" , \"urld url2\" ]) @pytest . mark . parametrize ( \"urllist\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"cmnt\" , [ \"Some Comment\" ]) @pytest . mark . parametrize ( \"srce\" , [ \"Do\" , \"Ra\" ]) def test_inter_edit_cli ( filemeta2 , announce , cmnt , srce , urllist , monkeypatch ): \"\"\" Test editing torrent interactively from CLI. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , cmnt , \"2\" , srce , \"5\" , urllist , urllist , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) sys . argv = [ \"torrentfile\" , \"-i\" ] main () meta2 = pyben . load ( filemeta2 ) assert meta2 [ \"info\" ][ \"source\" ] == srce assert meta2 [ \"info\" ][ \"comment\" ] == cmnt assert meta2 [ \"url-list\" ] == urllist . split () assert meta2 [ \"info\" ][ \"private\" ] == 1","title":"test_inter_edit_cli()"},{"location":"source/#tests.test_interactive.test_inter_edit_full","text":"Test editing torrent file interactively. Source code in tests\\test_interactive.py 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 @pytest . mark . parametrize ( \"announce\" , [ \"url1\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Fa\" , \"So\" , \"La\" ]) def test_inter_edit_full ( filemeta2 , announce , comment , source , url_list , monkeypatch ): \"\"\" Test editing torrent file interactively. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , comment , \"2\" , source , \"5\" , url_list , \"\" , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta1 = pyben . load ( filemeta2 ) assert meta1 [ \"info\" ][ \"source\" ] == source assert meta1 [ \"info\" ][ \"comment\" ] == comment assert meta1 [ \"url-list\" ] == url_list . split () assert meta1 [ \"info\" ][ \"private\" ] == 1","title":"test_inter_edit_full()"},{"location":"source/#tests.test_interactive.test_inter_recheck","text":"Test interactive recheck function. Source code in tests\\test_interactive.py 180 181 182 183 184 185 186 187 188 189 190 191 @pytest . mark . parametrize ( \"torrentclass\" , torrents ()) def test_inter_recheck ( torrentclass , monkeypatch , file1 ): \"\"\" Test interactive recheck function. \"\"\" torrent = torrentclass ( path = file1 ) filemeta , _ = torrent . write () seq = [ \"recheck\" , filemeta , str ( file1 )] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) result = select_action () assert result == 100","title":"test_inter_recheck()"},{"location":"source/#tests.test_interactive.test_interactive_create","text":"Test creating torrent interactively. Source code in tests\\test_interactive.py 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 def test_interactive_create ( monkeypatch , file1 ): \"\"\" Test creating torrent interactively. \"\"\" mapping = [ \"create\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , file1 , str ( file1 ) + \".torrent\" , \"\" , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () assert os . path . exists ( str ( file1 ) + \".torrent\" )","title":"test_interactive_create()"},{"location":"source/#tests.test_recheck","text":"Testing functions for the progress module.","title":"test_recheck"},{"location":"source/#tests.test_recheck.test_checker_callback","text":"Test Checker class with directory that points to nothing. Source code in tests\\test_recheck.py 125 126 127 128 129 130 131 def test_checker_callback ( dir1 , metafile1 ): \"\"\" Test Checker class with directory that points to nothing. \"\"\" Checker . register_callback ( lambda * x : print ( x )) checker = Checker ( metafile1 , str ( dir1 )) assert checker . results () == 100","title":"test_checker_callback()"},{"location":"source/#tests.test_recheck.test_checker_class","text":"Test Checker Class against meta files. Source code in tests\\test_recheck.py 42 43 44 45 46 47 def test_checker_class ( dir1 , metafile1 ): \"\"\" Test Checker Class against meta files. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100","title":"test_checker_class()"},{"location":"source/#tests.test_recheck.test_checker_class_allfiles","text":"Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 def test_checker_class_allfiles ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" def traverse ( path ): \"\"\" Traverse internal subdirectories. \"\"\" if path . is_file (): rmpath ( path ) elif path . is_dir (): for item in path . iterdir (): traverse ( item ) traverse ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_class_allfiles()"},{"location":"source/#tests.test_recheck.test_checker_class_allpaths","text":"Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 211 212 213 214 215 216 217 218 def test_checker_class_allpaths ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" for item in Path ( str ( dir2 )) . iterdir (): rmpath ( item ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_class_allpaths()"},{"location":"source/#tests.test_recheck.test_checker_class_half_file","text":"Test Checker class with half size single file. Source code in tests\\test_recheck.py 221 222 223 224 225 226 227 228 229 230 231 232 def test_checker_class_half_file ( filemeta2 , file2 ): \"\"\" Test Checker class with half size single file. \"\"\" half = int ( os . path . getsize ( file2 ) / 2 ) barr = bytearray ( half ) with open ( file2 , \"rb\" ) as content : content . readinto ( barr ) with open ( file2 , \"wb\" ) as content : content . write ( barr ) checker = Checker ( filemeta2 , file2 ) assert int ( checker . results ()) != 10","title":"test_checker_class_half_file()"},{"location":"source/#tests.test_recheck.test_checker_cli_args","text":"Test exclusive Checker Mode CLI. Source code in tests\\test_recheck.py 134 135 136 137 138 139 140 def test_checker_cli_args ( dir1 , metafile1 ): \"\"\" Test exclusive Checker Mode CLI. \"\"\" sys . argv = [ \"torrentfile\" , \"check\" , str ( metafile1 ), str ( dir1 )] output = main () assert output == 100","title":"test_checker_cli_args()"},{"location":"source/#tests.test_recheck.test_checker_empty_files","text":"Test Checker when directory contains 0 length files. Source code in tests\\test_recheck.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 def test_checker_empty_files ( dir2 , sizedfiles ): \"\"\" Test Checker when directory contains 0 length files. \"\"\" def empty_files ( root ): \"\"\" Dump contents of files. \"\"\" if os . path . isfile ( root ): with open ( root , \"wb\" ) as _ : pass assert os . path . getsize ( root ) == 0 elif os . path . isdir ( root ): for item in os . listdir ( root ): return empty_files ( os . path . join ( root , item )) return root empty_files ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100","title":"test_checker_empty_files()"},{"location":"source/#tests.test_recheck.test_checker_first_piece","text":"Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 def test_checker_first_piece ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if path . is_file (): new = b \"Something other than what was there before.\" with open ( path , \"rb\" ) as bfile : data = bfile . read () new_len = len ( new ) content = b \"\" . join ([ new , data [ new_len :]]) with open ( path , \"wb\" ) as bdoc : bdoc . write ( content ) elif path . is_dir (): for item in path . iterdir (): change ( item ) change ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100","title":"test_checker_first_piece()"},{"location":"source/#tests.test_recheck.test_checker_first_piece_alt","text":"Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 def test_checker_first_piece_alt ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if os . path . isfile ( path ): with open ( path , \"rb\" ) as bfile : data = bfile . read () new = b \"some_other_bytes_to_use\" new_len = len ( new ) with open ( path , \"wb\" ) as wfile : wfile . write ( new + data [ new_len :]) elif os . path . isdir ( path ): for item in os . listdir ( path ): change ( os . path . join ( path , item )) change ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100","title":"test_checker_first_piece_alt()"},{"location":"source/#tests.test_recheck.test_checker_missing","text":"Test Checker class when files are missing from contents. Source code in tests\\test_recheck.py 179 180 181 182 183 184 185 186 187 188 def test_checker_missing ( sizedfiles , dir2 ): \"\"\" Test Checker class when files are missing from contents. \"\"\" count = 0 for fd in Path ( dir2 ) . iterdir (): if fd . is_file () and count < 2 : rmpath ( fd ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_missing()"},{"location":"source/#tests.test_recheck.test_checker_missing_singles","text":"Test Checker class with half size single file. Source code in tests\\test_recheck.py 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def test_checker_missing_singles ( dir2 , sizedfiles ): \"\"\" Test Checker class with half size single file. \"\"\" def walk ( root ): \"\"\" Remove first file found. \"\"\" if root . is_file (): rmpath ( root ) return True if root . is_dir (): for item in root . iterdir (): walk ( item ) return False walk ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_missing_singles()"},{"location":"source/#tests.test_recheck.test_checker_no_meta_file","text":"Test Checker when incorrect metafile is provided. Source code in tests\\test_recheck.py 159 160 161 162 163 164 165 166 def test_checker_no_meta_file (): \"\"\" Test Checker when incorrect metafile is provided. \"\"\" try : Checker ( \"peaches\" , \"$\" ) except FileNotFoundError : assert True","title":"test_checker_no_meta_file()"},{"location":"source/#tests.test_recheck.test_checker_parent_dir","text":"Test providing the parent directory for torrent checking feature. Source code in tests\\test_recheck.py 143 144 145 146 147 148 def test_checker_parent_dir ( dir1 , metafile1 ): \"\"\" Test providing the parent directory for torrent checking feature. \"\"\" checker = Checker ( metafile1 , os . path . dirname ( dir1 )) assert checker . results () == 100","title":"test_checker_parent_dir()"},{"location":"source/#tests.test_recheck.test_checker_result_property","text":"Test Checker class with half size single file. Source code in tests\\test_recheck.py 257 258 259 260 261 262 263 def test_checker_result_property ( dir1 , metafile1 ): \"\"\" Test Checker class with half size single file. \"\"\" checker = Checker ( metafile1 , dir1 ) result = checker . results () assert checker . results () == result","title":"test_checker_result_property()"},{"location":"source/#tests.test_recheck.test_checker_simplest","text":"Test the simplest example. Source code in tests\\test_recheck.py 266 267 268 269 270 271 def test_checker_simplest ( dir1 , metafile1 ): \"\"\" Test the simplest example. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100","title":"test_checker_simplest()"},{"location":"source/#tests.test_recheck.test_checker_with_file","text":"Test checker with single file torrent. Source code in tests\\test_recheck.py 151 152 153 154 155 156 def test_checker_with_file ( file1 , filemeta1 ): \"\"\" Test checker with single file torrent. \"\"\" checker = Checker ( filemeta1 , file1 ) assert checker . results () == 100","title":"test_checker_with_file()"},{"location":"source/#tests.test_recheck.test_checker_wrong_root_dir","text":"Test Checker when incorrect root directory is provided. Source code in tests\\test_recheck.py 169 170 171 172 173 174 175 176 def test_checker_wrong_root_dir ( metafile1 ): \"\"\" Test Checker when incorrect root directory is provided. \"\"\" try : Checker ( metafile1 , \"fake\" ) except FileNotFoundError : assert True","title":"test_checker_wrong_root_dir()"},{"location":"source/#tests.test_recheck.test_fixtures","text":"Test fixtures exist. Source code in tests\\test_recheck.py 33 34 35 36 37 38 39 def test_fixtures (): \"\"\" Test fixtures exist. \"\"\" assert dir1 and dir2 and file1 and file2 assert filemeta1 and filemeta2 and metafile1 assert metafile2 and sizes and sizedfiles","title":"test_fixtures()"},{"location":"source/#tests.test_recheck.test_partial_metafiles","text":"Test Checker with data that is expected to be incomplete. Source code in tests\\test_recheck.py 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 def test_partial_metafiles ( dir2 , sizedfiles ): \"\"\" Test Checker with data that is expected to be incomplete. \"\"\" def shortenfile ( path ): \"\"\" Shorten a few files for testing purposes. \"\"\" with open ( path , \"rb\" ) as bfile : data = bfile . read () with open ( path , \"wb\" ) as bfile : bfile . write ( data [: - ( 2 ** 10 )]) for item in os . listdir ( dir2 ): full = os . path . join ( dir2 , item ) if os . path . isfile ( full ): shortenfile ( full ) testdir = os . path . dirname ( dir2 ) checker = Checker ( sizedfiles , testdir ) assert checker . results () != 100","title":"test_partial_metafiles()"},{"location":"source/#tests.test_recheck.test_recheck_wrong_dir","text":"Test recheck function with directory that doesn't contain the contents. Source code in tests\\test_recheck.py 297 298 299 300 301 302 303 304 305 def test_recheck_wrong_dir ( metafile1 ): \"\"\" Test recheck function with directory that doesn't contain the contents. \"\"\" grandparent = os . path . dirname ( os . path . dirname ( metafile1 )) try : _ = Checker ( metafile1 , grandparent ) except FileNotFoundError : assert True","title":"test_recheck_wrong_dir()"},{"location":"source/#tests.test_torrent","text":"Testing functions for the torrent module.","title":"test_torrent"},{"location":"source/#tests.test_torrent.test_create_cwd_fail","text":"Test cwd argument with create command failure. Source code in tests\\test_torrent.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_create_cwd_fail (): \"\"\"Test cwd argument with create command failure.\"\"\" class SuFile : \"\"\"A mock admin file.\"\"\" @staticmethod def __fspath__ (): raise PermissionError def __str__ ( self ): return \"SuFile\" tfile = tempfile () name = os . path . basename ( tfile ) + \".torrent\" torrent = MetaFile ( path = tfile ) sufile = SuFile () torrent . write ( outfile = sufile ) current = os . path . join ( \".\" , name ) assert os . path . exists ( current ) rmpath ( tfile , current )","title":"test_create_cwd_fail()"},{"location":"source/#tests.test_torrent.test_fixtures","text":"Test pytest fixtures. Source code in tests\\test_torrent.py 32 33 34 35 36 def test_fixtures (): \"\"\" Test pytest fixtures. \"\"\" assert dir1 and dir2","title":"test_fixtures()"},{"location":"source/#tests.test_torrent.test_mbtorrent","text":"Test torrent creation for file size larger than 10MB. Source code in tests\\test_torrent.py 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 @pytest . mark . parametrize ( \"version\" , torrents ()) @pytest . mark . parametrize ( \"progress\" , [ 0 , 1 , 2 ]) def test_mbtorrent ( version , progress ): \"\"\" Test torrent creation for file size larger than 10MB. \"\"\" tfile = tempfile ( exp = 26 ) args = { \"path\" : tfile , \"progress\" : progress , \"piece_length\" : \"14\" , } torrent = version ( ** args ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile )","title":"test_mbtorrent()"},{"location":"source/#tests.test_torrent.test_metafile_assemble","text":"Test assembling base metafile exception. Source code in tests\\test_torrent.py 50 51 52 53 54 55 56 57 58 def test_metafile_assemble ( dir1 ): \"\"\" Test assembling base metafile exception. \"\"\" metafile = MetaFile ( path = dir1 ) try : metafile . assemble () except NotImplementedError : assert True","title":"test_metafile_assemble()"},{"location":"source/#tests.test_torrent.test_torrentfile_extra","text":"Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_extra ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" def walk ( item ): \"\"\" Edit files in directory structure. \"\"\" if item . is_file (): with open ( item , \"ab\" ) as binfile : binfile . write ( bytes ( 1000 )) elif item . is_dir (): for sub in item . iterdir (): walk ( sub ) walk ( dir2 ) args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\"","title":"test_torrentfile_extra()"},{"location":"source/#tests.test_torrent.test_torrentfile_missing_path","text":"Test missing path error exception. Source code in tests\\test_torrent.py 39 40 41 42 43 44 45 46 47 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_missing_path ( version ): \"\"\" Test missing path error exception. \"\"\" try : version () except MissingPathError : assert True","title":"test_torrentfile_missing_path()"},{"location":"source/#tests.test_torrent.test_torrentfile_one_empty","text":"Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_one_empty ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" a = next ( os . walk ( dir2 )) if len ( a [ - 1 ]) > 0 : with open ( os . path . join ( a [ 0 ], a [ - 1 ][ 0 ]), \"w\" , encoding = \"utf-8\" ) as _ : pass args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\"","title":"test_torrentfile_one_empty()"},{"location":"source/#tests.test_torrent.test_torrentfile_single","text":"Test creating a torrent file from a single file contents. Source code in tests\\test_torrent.py 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 @pytest . mark . parametrize ( \"num\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single ( version , num , piece_length , capsys ): \"\"\" Test creating a torrent file from a single file contents. \"\"\" tfile = tempfile ( exp = num ) with capsys . disabled (): version . set_callback ( print ) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } trent = version ( ** args ) trent . write () assert os . path . exists ( str ( tfile ) + \".torrent\" ) rmpath ( tfile , str ( tfile ) + \".torrent\" )","title":"test_torrentfile_single()"},{"location":"source/#tests.test_torrent.test_torrentfile_single_extra","text":"Test creating a torrent file from a single file contents plus extra. Source code in tests\\test_torrent.py 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 @pytest . mark . parametrize ( \"size\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single_extra ( version , size , piece_length ): \"\"\" Test creating a torrent file from a single file contents plus extra. \"\"\" tfile = tempfile ( exp = size ) with open ( tfile , \"ab\" ) as binfile : binfile . write ( bytes ( str ( tfile ) . encode ( \"utf-8\" ))) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } torrent = version ( ** args ) torrent . write () outfile = str ( tfile ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( tfile , outfile )","title":"test_torrentfile_single_extra()"},{"location":"source/#tests.test_torrent.test_torrentfile_single_under","text":"Test creating a torrent file from less than a single file contents. Source code in tests\\test_torrent.py 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 @pytest . mark . parametrize ( \"sze\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piecelength\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"ver\" , torrents ()) def test_torrentfile_single_under ( ver , sze , piecelength ): \"\"\" Test creating a torrent file from less than a single file contents. \"\"\" tfile = tempfile ( exp = sze ) with open ( tfile , \"rb\" ) as binfile : data = binfile . read () with open ( tfile , \"wb\" ) as binfile : binfile . write ( data [: - ( 2 ** 9 )]) kwargs = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piecelength , } torrent = ver ( ** kwargs ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile )","title":"test_torrentfile_single_under()"},{"location":"source/#tests.test_torrent.test_waiting_mixin","text":"Test waiting function. Source code in tests\\test_torrent.py 198 199 200 201 202 203 204 205 206 def test_waiting_mixin (): \"\"\" Test waiting function. \"\"\" msg = \"Testing message\" lst = [] timeout = 3 waiting ( msg , lst , timeout = timeout ) assert len ( lst ) == 0","title":"test_waiting_mixin()"},{"location":"source/#tests.test_utils","text":"Unittest functions for testing torrentfile utils module.","title":"test_utils"},{"location":"source/#tests.test_utils.test_filelist_total","text":"Test function for acquiring a filelist for directory. Source code in tests\\test_utils.py 118 119 120 121 122 123 def test_filelist_total ( dir1 ): \"\"\" Test function for acquiring a filelist for directory. \"\"\" total , _ = utils . filelist_total ( dir1 ) assert total == ( 2 ** 18 ) * 8","title":"test_filelist_total()"},{"location":"source/#tests.test_utils.test_filelisttotal_missing","text":"Test function filelist total with missing path. Parameters: Name Type Description Default dir2 pytest . fixture fixture containing a temporary directory required Source code in tests\\test_utils.py 225 226 227 228 229 230 231 232 233 234 235 236 237 def test_filelisttotal_missing ( dir2 ): \"\"\"Test function filelist total with missing path. Parameters ---------- dir2 : pytest.fixture fixture containing a temporary directory \"\"\" rmpath ( dir2 ) try : utils . filelist_total ( dir2 ) except utils . MissingPathError : assert True","title":"test_filelisttotal_missing()"},{"location":"source/#tests.test_utils.test_get_filelist","text":"Test function for get a list of files in a directory. Source code in tests\\test_utils.py 102 103 104 105 106 107 def test_get_filelist ( dir1 ): \"\"\" Test function for get a list of files in a directory. \"\"\" filelist = utils . get_file_list ( dir1 ) assert len ( filelist ) == 8","title":"test_get_filelist()"},{"location":"source/#tests.test_utils.test_get_path_length_max","text":"Test function for getting piece length for folders max. Source code in tests\\test_utils.py 71 72 73 74 75 def test_get_path_length_max ( dir1 ): \"\"\" Test function for getting piece length for folders max. \"\"\" assert utils . path_piece_length ( dir1 ) <= ( 2 ** 27 )","title":"test_get_path_length_max()"},{"location":"source/#tests.test_utils.test_get_path_length_min","text":"Test function for getting piece length for folders min. Source code in tests\\test_utils.py 64 65 66 67 68 def test_get_path_length_min ( dir1 ): \"\"\" Test function for getting piece length for folders min. \"\"\" assert utils . path_piece_length ( dir1 ) >= ( 2 ** 14 )","title":"test_get_path_length_min()"},{"location":"source/#tests.test_utils.test_get_path_length_mod","text":"Test function for the best piece length for provided path. Source code in tests\\test_utils.py 57 58 59 60 61 def test_get_path_length_mod ( dir1 ): \"\"\" Test function for the best piece length for provided path. \"\"\" assert utils . path_piece_length ( dir1 ) % ( 2 ** 14 ) == 0","title":"test_get_path_length_mod()"},{"location":"source/#tests.test_utils.test_get_path_size","text":"Test function for getting total size of directory. Source code in tests\\test_utils.py 110 111 112 113 114 115 def test_get_path_size ( dir1 ): \"\"\" Test function for getting total size of directory. \"\"\" pathsize = utils . path_size ( dir1 ) assert pathsize == ( 2 ** 18 ) * 8","title":"test_get_path_size()"},{"location":"source/#tests.test_utils.test_get_piece_length","text":"Test function for best piece length for given size. Source code in tests\\test_utils.py 30 31 32 33 34 35 36 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length ( size ): \"\"\" Test function for best piece length for given size. \"\"\" value = utils . get_piece_length ( size ) assert value % 1024 == 0","title":"test_get_piece_length()"},{"location":"source/#tests.test_utils.test_get_piece_length_max","text":"Test function for best piece length for given size maximum. Source code in tests\\test_utils.py 39 40 41 42 43 44 45 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_max ( size ): \"\"\" Test function for best piece length for given size maximum. \"\"\" value = utils . get_piece_length ( size ) assert value < 2 ** 27","title":"test_get_piece_length_max()"},{"location":"source/#tests.test_utils.test_get_piece_length_min","text":"Test function for best piece length for given size minimum. Source code in tests\\test_utils.py 48 49 50 51 52 53 54 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_min ( size ): \"\"\" Test function for best piece length for given size minimum. \"\"\" value = utils . get_piece_length ( size ) assert value >= 2 ** 14","title":"test_get_piece_length_min()"},{"location":"source/#tests.test_utils.test_humanize_bytes","text":"Test humanize bytes function. Source code in tests\\test_utils.py 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 @pytest . mark . parametrize ( \"amount, result\" , [ ( 100 , \"100\" ), ( 1100 , \"1 KiB\" ), ( 1_100_000 , \"1 MiB\" ), ( 1_100_000_000 , \"1 GiB\" ), ( 4_400_120_000 , \"4 GiB\" ), ( 4_000_120_000 , \"3 GiB\" ), ], ) def test_humanize_bytes ( amount , result ): \"\"\" Test humanize bytes function. \"\"\" assert utils . humanize_bytes ( amount ) == result","title":"test_humanize_bytes()"},{"location":"source/#tests.test_utils.test_missing_path_error","text":"Test exception for missing path parameter. Source code in tests\\test_utils.py 137 138 139 140 141 142 143 144 145 def test_missing_path_error (): \"\"\" Test exception for missing path parameter. \"\"\" try : raise utils . MissingPathError ( \"message\" ) except utils . MissingPathError : assert True assert dir2","title":"test_missing_path_error()"},{"location":"source/#tests.test_utils.test_next_power_2","text":"Test next power of 2 function in utils module. Source code in tests\\test_utils.py 148 149 150 151 152 153 154 155 156 157 @pytest . mark . parametrize ( \"value\" , [ 5 , 32 , 18 , 225 , 16384 , 256000 ]) def test_next_power_2 ( value ): \"\"\" Test next power of 2 function in utils module. \"\"\" result = utils . next_power_2 ( value ) log = math . log2 ( result ) assert log == int ( log ) assert result % 2 == 0 assert result >= value","title":"test_next_power_2()"},{"location":"source/#tests.test_utils.test_norm_plength_errors","text":"Test function to normalize piece length errors. Parameters: Name Type Description Default amount any arguments intended to raise an exception. required Source code in tests\\test_utils.py 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"amount\" , [ \"hello\" , 11 , 0 , 100000 , 28 , \"zero\" , \"fifteen\" ] ) def test_norm_plength_errors ( amount ): \"\"\"Test function to normalize piece length errors. Parameters ---------- amount : any arguments intended to raise an exception. \"\"\" try : assert utils . normalize_piece_length ( amount ) except utils . PieceLengthValueError : assert True","title":"test_norm_plength_errors()"},{"location":"source/#tests.test_utils.test_normalize_piece_length_int","text":"Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 178 179 180 181 182 183 184 185 186 187 188 189 @pytest . mark . parametrize ( \"amount, result\" , [( i , 2 ** i ) for i in range ( 14 , 25 )]) def test_normalize_piece_length_int ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result","title":"test_normalize_piece_length_int()"},{"location":"source/#tests.test_utils.test_normalize_piece_length_str","text":"Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 192 193 194 195 196 197 198 199 200 201 202 203 204 205 @pytest . mark . parametrize ( \"amount, result\" , [( str ( i ), 2 ** i ) for i in range ( 14 , 21 )] ) def test_normalize_piece_length_str ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result","title":"test_normalize_piece_length_str()"},{"location":"source/#tests.test_utils.test_path_stat","text":"Test function for acquiring piece length information on folder. Source code in tests\\test_utils.py 78 79 80 81 82 83 def test_path_stat ( dir1 ): \"\"\" Test function for acquiring piece length information on folder. \"\"\" _ , _ , piece_length = utils . path_stat ( dir1 ) assert piece_length % ( 2 ** 14 ) == 0","title":"test_path_stat()"},{"location":"source/#tests.test_utils.test_path_stat_filelist_size","text":"Test function for acquiring file list information on folder. Source code in tests\\test_utils.py 94 95 96 97 98 99 def test_path_stat_filelist_size ( dir1 ): \"\"\" Test function for acquiring file list information on folder. \"\"\" filelist , _ , _ = utils . path_stat ( dir1 ) assert len ( filelist ) == 8","title":"test_path_stat_filelist_size()"},{"location":"source/#tests.test_utils.test_path_stat_size","text":"Test function for acquiring total size information on folder. Source code in tests\\test_utils.py 86 87 88 89 90 91 def test_path_stat_size ( dir1 ): \"\"\" Test function for acquiring total size information on folder. \"\"\" _ , totalsize , _ = utils . path_stat ( dir1 ) assert totalsize == ( 2 ** 18 ) * 8","title":"test_path_stat_size()"},{"location":"source/#tests.test_utils.test_piecelength_error_fixtures","text":"Test exception for uninterpretable piece length value. Source code in tests\\test_utils.py 126 127 128 129 130 131 132 133 134 def test_piecelength_error_fixtures (): \"\"\" Test exception for uninterpretable piece length value. \"\"\" try : raise utils . PieceLengthValueError ( \"message\" ) except utils . PieceLengthValueError : assert True assert dir1","title":"test_piecelength_error_fixtures()"}]} \ No newline at end of file +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"TorrentFile \ud83c\udf10 Overview A simple and convenient tool for creating, reviewing, editing, and/or validating bittorrent meta files (aka torrent files). torrentfile supports all versions of Bittorrent files, including hybrid meta files, and has full unicode support. A GUI frontend for this project can be found at https://github.com/alexpdev/TorrentfileQt \ud83d\udd0c Requirements Python 3.7+ Tested on Linux, Windows and Mac \ud83d\udcbb Install via PyPi: pip install torrentfile via Git: git clone https://github.com/alexpdev/torrentfile.git cd torrentfile pip install . Download pre-compiled binaries from the release page . \ud83d\udcda Documentation Documentation can be found here or in the docs directory. torrentfile is under active development, and is subject to significant changes in the codebase between releases. \ud83d\ude80 Usage Usage ===== torrentfile [options] [command-options] ... Create, edit, show details, and check any version Bittorrent file from command line Commands -------- create (c, new) Create a torrent meta file. edit (e) Edit existing torrent meta file. magnet (m) Create magnet url from an existing Bittorrent meta file. recheck (r, check) Calculate amount of torrent meta files content is found on disk. info (i) Show detailed information about a torrent file. Usage examples can be found in the project documentation on the examples page. \ud83d\udcdd License Distributed under Apache v2 software license. See LICENSE for more information. \ud83d\udca1 Issues & Requests & PRs If you encounter any bugs or would like to request a new feature please open a new issue. PRs and other contributions are welcome https://github.com/alexpdev/torrentfile/issues Creating Torrents Basic torrent file createion > torrentfile create /path/to/content The -t --tracker -a --announce flags add one or more urls to list of trackers. > torrentfile create /path/to/content --tracker http://tracker1.com > torrentfile create -t http://tracker2 http://tracker3 --private /path/to/content > torrentfile create --tracker http://tracker /path/to/content > torrentfile create -t http://tracker1 http://tracker2 /path/to/content the --private flag indicates use by a private tracker the --source flag can be used to help with cross-seeding > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 The options for controlling the progress bar using --prog or --progress : 0 : show no progress bar at all 1 : show progress bar (default) > torrentfile -t http://tracker.com --progress 2 /path/to/content > torrentfile --prog 0 /path/to/content to specify the save location use the -o or --out flags if the path points to directory the name of torrent is autofilled. > torrentfile create -o /specific/path/name.torrent ./content to create files using bittorrent v2 use --meta-version 2 likewise --meta-version 3 creates a hybrid torrent file. > torrentfile create --meta-version 2 /path/to/content > torrentfile create --meta-version 3 /path/to/content Check/Recheck Torrent recheck torrent file /path/to/some.torrent with /path/to/content > torrentfile recheck /path/to/some.torrent /path/to/content Edit Torrent edit a torrent file > torrentfile edit [options] Create Magnet To create a magnet URI for a pre-existing torrent meta file, use the sub-command magnet or m with the path to the torrent file. > torrentfile magnet /path/to/some.torrent Interactive Mode (expiremental) Alternatively to supplying a bunch of command line arguments, interactive mode allows users to specify program options one at a time from a series of prompts. to activate interactive mode use -i or --interactive flag > torrentfile -i GUI If you prefer a windowed gui please check out the official GUI frontend here","title":"Home"},{"location":"#torrentfile","text":"","title":"TorrentFile"},{"location":"#overview","text":"A simple and convenient tool for creating, reviewing, editing, and/or validating bittorrent meta files (aka torrent files). torrentfile supports all versions of Bittorrent files, including hybrid meta files, and has full unicode support. A GUI frontend for this project can be found at https://github.com/alexpdev/TorrentfileQt","title":"\ud83c\udf10 Overview"},{"location":"#requirements","text":"Python 3.7+ Tested on Linux, Windows and Mac","title":"\ud83d\udd0c Requirements"},{"location":"#install","text":"via PyPi: pip install torrentfile via Git: git clone https://github.com/alexpdev/torrentfile.git cd torrentfile pip install . Download pre-compiled binaries from the release page .","title":"\ud83d\udcbb Install"},{"location":"#documentation","text":"Documentation can be found here or in the docs directory. torrentfile is under active development, and is subject to significant changes in the codebase between releases.","title":"\ud83d\udcda Documentation"},{"location":"#usage","text":"Usage ===== torrentfile [options] [command-options] ... Create, edit, show details, and check any version Bittorrent file from command line Commands -------- create (c, new) Create a torrent meta file. edit (e) Edit existing torrent meta file. magnet (m) Create magnet url from an existing Bittorrent meta file. recheck (r, check) Calculate amount of torrent meta files content is found on disk. info (i) Show detailed information about a torrent file. Usage examples can be found in the project documentation on the examples page.","title":"\ud83d\ude80 Usage"},{"location":"#license","text":"Distributed under Apache v2 software license. See LICENSE for more information.","title":"\ud83d\udcdd License"},{"location":"#issues-requests-prs","text":"If you encounter any bugs or would like to request a new feature please open a new issue. PRs and other contributions are welcome https://github.com/alexpdev/torrentfile/issues","title":"\ud83d\udca1 Issues & Requests & PRs"},{"location":"#creating-torrents","text":"Basic torrent file createion > torrentfile create /path/to/content The -t --tracker -a --announce flags add one or more urls to list of trackers. > torrentfile create /path/to/content --tracker http://tracker1.com > torrentfile create -t http://tracker2 http://tracker3 --private /path/to/content > torrentfile create --tracker http://tracker /path/to/content > torrentfile create -t http://tracker1 http://tracker2 /path/to/content the --private flag indicates use by a private tracker the --source flag can be used to help with cross-seeding > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 The options for controlling the progress bar using --prog or --progress : 0 : show no progress bar at all 1 : show progress bar (default) > torrentfile -t http://tracker.com --progress 2 /path/to/content > torrentfile --prog 0 /path/to/content to specify the save location use the -o or --out flags if the path points to directory the name of torrent is autofilled. > torrentfile create -o /specific/path/name.torrent ./content to create files using bittorrent v2 use --meta-version 2 likewise --meta-version 3 creates a hybrid torrent file. > torrentfile create --meta-version 2 /path/to/content > torrentfile create --meta-version 3 /path/to/content","title":"Creating Torrents"},{"location":"#checkrecheck-torrent","text":"recheck torrent file /path/to/some.torrent with /path/to/content > torrentfile recheck /path/to/some.torrent /path/to/content","title":"Check/Recheck Torrent"},{"location":"#edit-torrent","text":"edit a torrent file > torrentfile edit [options] ","title":"Edit Torrent"},{"location":"#create-magnet","text":"To create a magnet URI for a pre-existing torrent meta file, use the sub-command magnet or m with the path to the torrent file. > torrentfile magnet /path/to/some.torrent","title":"Create Magnet"},{"location":"#interactive-mode-expiremental","text":"Alternatively to supplying a bunch of command line arguments, interactive mode allows users to specify program options one at a time from a series of prompts. to activate interactive mode use -i or --interactive flag > torrentfile -i","title":"Interactive Mode (expiremental)"},{"location":"#gui","text":"If you prefer a windowed gui please check out the official GUI frontend here","title":"GUI"},{"location":"Apache2/","text":"Apache License Version 2.0, January 2004 < http://www.apache.org/licenses/ > Terms and Conditions for use, reproduction, and distribution 1. Definitions \u201cLicense\u201d shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. \u201cLicensor\u201d shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. \u201cLegal Entity\u201d shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \u201ccontrol\u201d means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. \u201cYou\u201d (or \u201cYour\u201d) shall mean an individual or Legal Entity exercising permissions granted by this License. \u201cSource\u201d form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. \u201cObject\u201d form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. \u201cWork\u201d shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). \u201cDerivative Works\u201d shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. \u201cContribution\u201d shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \u201csubmitted\u201d means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \u201cNot a Contribution.\u201d \u201cContributor\u201d shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a \u201cNOTICE\u201d text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \u201cAS IS\u201d BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets [] replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \u201cprinted page\u201d as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"License"},{"location":"Apache2/#apache-license","text":"Version 2.0, January 2004 < http://www.apache.org/licenses/ >","title":"Apache License"},{"location":"Apache2/#terms-and-conditions-for-use-reproduction-and-distribution","text":"","title":"Terms and Conditions for use, reproduction, and distribution"},{"location":"Apache2/#1-definitions","text":"\u201cLicense\u201d shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. \u201cLicensor\u201d shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. \u201cLegal Entity\u201d shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \u201ccontrol\u201d means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. \u201cYou\u201d (or \u201cYour\u201d) shall mean an individual or Legal Entity exercising permissions granted by this License. \u201cSource\u201d form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. \u201cObject\u201d form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. \u201cWork\u201d shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). \u201cDerivative Works\u201d shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. \u201cContribution\u201d shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \u201csubmitted\u201d means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \u201cNot a Contribution.\u201d \u201cContributor\u201d shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.","title":"1. Definitions"},{"location":"Apache2/#2-grant-of-copyright-license","text":"Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.","title":"2. Grant of Copyright License"},{"location":"Apache2/#3-grant-of-patent-license","text":"Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.","title":"3. Grant of Patent License"},{"location":"Apache2/#4-redistribution","text":"You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a \u201cNOTICE\u201d text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.","title":"4. Redistribution"},{"location":"Apache2/#5-submission-of-contributions","text":"Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.","title":"5. Submission of Contributions"},{"location":"Apache2/#6-trademarks","text":"This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.","title":"6. Trademarks"},{"location":"Apache2/#7-disclaimer-of-warranty","text":"Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \u201cAS IS\u201d BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.","title":"7. Disclaimer of Warranty"},{"location":"Apache2/#8-limitation-of-liability","text":"In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.","title":"8. Limitation of Liability"},{"location":"Apache2/#9-accepting-warranty-or-additional-liability","text":"While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS","title":"9. Accepting Warranty or Additional Liability"},{"location":"Apache2/#appendix-how-to-apply-the-apache-license-to-your-work","text":"To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets [] replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \u201cprinted page\u201d as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"APPENDIX: How to apply the Apache License to your work"},{"location":"Commands/","text":"TorrentFile CLI Menu Help Messages Main Usage ===== torrentfile [-h] [-i] [-V] [-v] ... CLI Tool for creating, checking, editing... Bittorrent meta files. TorrentFile supports all versions of torrent files. Options ------- -h, --help show this help message and exit -i, --interactive select program options interactively -V, --version show program version and exit -v, --verbose output debug information Actions ------- c (create, new) Create a torrent meta file. e (edit) Edit existing torrent meta file. m (magnet) Create magnet url from an existing Bittorrent meta file. r (recheck, check) Calculate amount of torrent meta file's content is found on disk. i (info) Show detailed information about a torrent file. Create Usage ===== torrentfile c [-h] [-a [ ...]] [-p] [-s ] [-m] [-c ] [-o ] [-t [ ...]] [--noprogress] [--meta-version ] [--piece-length ] [-w [ ...]] Positional Arguments -------------------- Path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit -a [ ...], --announce [ ...] Alias for -t/--tracker -p, --private Create a private torrent file -s , --source Useful for cross-seeding -m, --magnet Output Magnet Link after creation completes -c , --comment Include a comment in file metadata -o , --out Output path for created .torrent file -t [ ...], --tracker [ ...] One or more Bittorrent tracker announce url(s). --prog, --progress (0) = no progress bar displayed (1) = progress bar is displayed (default) --meta-version Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid --piece-length Number of bytes per piece. (Default: None) Acceptable inputs include {14 - 24} as exponent for 2^n, or any acceptable integer value (must be power of 2). Examples:: [--piece-length 14] [--piece-length 16777216] -w [ ...], --web-seed [ ...] One or more url(s) linking to a http server hosting the torrent contents. This is useful if the torrent tracker is ever unreachable. Example:: -w url1 url2 url3 Edit Usage ===== torrentfile e [-h] [--tracker [ ...]] [--web-seed [ ...]] [--private] [--comment ] [--source ] <*.torrent> Positional Arguments -------------------- <*.torrent> path to *.torrent file Optional Arguments ------------------ -h, --help show this help message and exit --tracker [ ...] Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). --web-seed [ ...] Replace current list of web-seed urls with one or more space seperated url(s) --private Make torrent private. --comment Replaces any existing comment with --source Replaces current source with Recheck Usage ===== torrentfile r [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> path to .torrent file. path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit Magnet Usage ===== torrentfile m [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> Path to Bittorrent meta file. Optional Arguments ------------------ -h, --help show this help message and exit","title":"Help Messages"},{"location":"Commands/#torrentfile-cli-menu","text":"","title":"TorrentFile CLI Menu"},{"location":"Commands/#help-messages","text":"","title":"Help Messages"},{"location":"Commands/#main","text":"Usage ===== torrentfile [-h] [-i] [-V] [-v] ... CLI Tool for creating, checking, editing... Bittorrent meta files. TorrentFile supports all versions of torrent files. Options ------- -h, --help show this help message and exit -i, --interactive select program options interactively -V, --version show program version and exit -v, --verbose output debug information Actions ------- c (create, new) Create a torrent meta file. e (edit) Edit existing torrent meta file. m (magnet) Create magnet url from an existing Bittorrent meta file. r (recheck, check) Calculate amount of torrent meta file's content is found on disk. i (info) Show detailed information about a torrent file.","title":"Main"},{"location":"Commands/#create","text":"Usage ===== torrentfile c [-h] [-a [ ...]] [-p] [-s ] [-m] [-c ] [-o ] [-t [ ...]] [--noprogress] [--meta-version ] [--piece-length ] [-w [ ...]] Positional Arguments -------------------- Path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit -a [ ...], --announce [ ...] Alias for -t/--tracker -p, --private Create a private torrent file -s , --source Useful for cross-seeding -m, --magnet Output Magnet Link after creation completes -c , --comment Include a comment in file metadata -o , --out Output path for created .torrent file -t [ ...], --tracker [ ...] One or more Bittorrent tracker announce url(s). --prog, --progress (0) = no progress bar displayed (1) = progress bar is displayed (default) --meta-version Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid --piece-length Number of bytes per piece. (Default: None) Acceptable inputs include {14 - 24} as exponent for 2^n, or any acceptable integer value (must be power of 2). Examples:: [--piece-length 14] [--piece-length 16777216] -w [ ...], --web-seed [ ...] One or more url(s) linking to a http server hosting the torrent contents. This is useful if the torrent tracker is ever unreachable. Example:: -w url1 url2 url3","title":"Create"},{"location":"Commands/#edit","text":"Usage ===== torrentfile e [-h] [--tracker [ ...]] [--web-seed [ ...]] [--private] [--comment ] [--source ] <*.torrent> Positional Arguments -------------------- <*.torrent> path to *.torrent file Optional Arguments ------------------ -h, --help show this help message and exit --tracker [ ...] Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). --web-seed [ ...] Replace current list of web-seed urls with one or more space seperated url(s) --private Make torrent private. --comment Replaces any existing comment with --source Replaces current source with ","title":"Edit"},{"location":"Commands/#recheck","text":"Usage ===== torrentfile r [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> path to .torrent file. path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit","title":"Recheck"},{"location":"Commands/#magnet","text":"Usage ===== torrentfile m [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> Path to Bittorrent meta file. Optional Arguments ------------------ -h, --help show this help message and exit","title":"Magnet"},{"location":"changelog/","text":"TorrentFile Version 0.7.9 complete rewrite of the recheck procedures Recheck now provides more accuracy and more details improvements to the new custom progressbar changed the cli argument for the progress bar the options are now just 0 and 1 included new unit tests for all new features marked unused functions as deprecated added a new hasher object for v2 and hybrid torrents minor bug fixes and styling changes Version 0.7.8 more updates to logging major improvements to progress bar removed tqdm as dependency implemented custom progress bar logic new cli argument controlling the progress bar support for pyben 0.3.1 added threading to recheck module added mixins module unit test updates and improvements Version 0.7.5 updates to logging facility fixed bug in created hybrid torrent files fixed cli when subcomman not chosen doc updates unit test updates and improvements Version 0.7.2 cleaned up readme and help messages removed useless print statements improved CI tooling and checking minor bug fixes Version 0.7.1 split CI integration into separate platform specific files added new cli argument --cwd which changes the default save to location to the current working directory (this will be default in future) added unit tests to cover the new argument Changed license to a the more permissive Apache 2 software license Version 0.7.0 Fixed issues with logging to file. Finished adding tests for Unicode Support Deprecated some unneccessary code Clean up documentation and README removed config files no longer in use. Version 0.6.13 Fixed bug that created a torrent file with no name. Fixed bug that would error if cli path was listed after announce urls Added full unicode support. Added Unittests for new features and bug fixes Version 0.6.11 Fixed bug that occured during recheck when file of 0 length is included. Altered Recheck algorithm to process 0 length files. Only effected meta version 2 and hybrid torrent files. Added unittests to cover the situation. Version 0.6.10 Updates to documentation Integrated Type hints in source code Updated build and CI process Version 0.6.9 The --progress flag is now --noprogress Default behavior is to show progress bar use --noprogress to not show added CLI Help format strings added custom CLI help formatter class Titled Help Message section headers Fixed a bunch of error pages created by mkdocs Version 0.6.8 Documentation for newest features CLI usage examples Improved unittests made progress bar active by default Version 0.6.7 Updates to API Version 0.6.6 bug that created faulty Bittorrent V2 meta files in some instances. back to working as it should. Version 0.6.5 Support for creating Magnet URI's Added optional progress bar for torrent creation Log File handler CLI args page in documentation verbose and logging bugs multi tracker errors bug Version 0.6.4 CLI interface add subcommands added interactive mode Re-wrote the recheck module fixed documentation and docstrings linting and testing errors Version 0.6.3 Fixed Bug that would format list of trackers incorrectly CLI Bug Fixes Version 0.6.2 Bug fixes Documentation error pages Version 0.6.0 cli commands alterations debug logging during creation process Version 0.5.2 Fixed Bug that was adding wrong fields to info dict Version 0.5.0 Slew of new unit tests Stricter linting features Alternative method of -re-check feature Bug Fixes CLI help formatting errors Version 0.4.8 Improved Algorithm performance for ReCheck. Additions to documentation. Version 0.4.7 Fixed A bug that misspelled a field when creating Hybrid torrent files. Re-Check procedure for v2 and hybrid torrent file checking. Version 0.4.6 CLI Help and Usage Messages. Expanded CLI args. Completely new CheckerClass which replaces old Checker Hooks for GUI or other 3rd party apps to hook into Checking Documentation and Unit tests for new CheckerClass Version 0.4.5 Documentation and docstrings improvements Better code formating and more detailed docstrings More unit tests. Version 0.4.2 The ReChecker feature now supports v1, v2, & hybrid .torrent file. Bug in CLI for python < 3.8 Version 0.4.1 Added tests for hybrid class Added logging features new cli flag to activate debug mode Documentation theme. Fixed Bug that allowed improper piece length values. Version 0.4.0 Fixed bugs in creating hybrid files. Bug Fix that broke cli. Version 0.3.0 Added/Improved support for hybrid meta files. Many additions to testing suit including linting and coverage tests. Version 0.2.8 Styling fixes. Bug Fixes. Prelimenary support for bittorrent hybrid meta files. Bug Fixes Version 0.2.7 major imporvements to torrentfile-GUI. minor adjustments to this package for integration. Code consolidation Bug Fixes Documentation additions Implemented CI/CD Integration Version 0.2.3 Bug Fixes Code Style and Formatting Added more unittests Version 0.2.1 Bittorrent Protocol V2 Support v2 metafile options to cli v2 metafile tests Version 0.1.7 Docstrings Improvements. Added documentation rederer. Improved readme file. formatting Version 0.1.2 Added a Command Line Interface Rough Graphical User Interface Minor Bug Fixes Improved unittest coverage Version 0.1.0 added SHA256 support Feeder class for seemless file switching Fixed the primary entrypoint function. Improved docstrings Bug fixes Version 0.0.2 Added Unittests added bencode support added hashing support Version 0.0.1 Initial concept and planning","title":"Changelog"},{"location":"changelog/#torrentfile","text":"","title":"TorrentFile"},{"location":"changelog/#version-079","text":"complete rewrite of the recheck procedures Recheck now provides more accuracy and more details improvements to the new custom progressbar changed the cli argument for the progress bar the options are now just 0 and 1 included new unit tests for all new features marked unused functions as deprecated added a new hasher object for v2 and hybrid torrents minor bug fixes and styling changes","title":"Version 0.7.9"},{"location":"changelog/#version-078","text":"more updates to logging major improvements to progress bar removed tqdm as dependency implemented custom progress bar logic new cli argument controlling the progress bar support for pyben 0.3.1 added threading to recheck module added mixins module unit test updates and improvements","title":"Version 0.7.8"},{"location":"changelog/#version-075","text":"updates to logging facility fixed bug in created hybrid torrent files fixed cli when subcomman not chosen doc updates unit test updates and improvements","title":"Version 0.7.5"},{"location":"changelog/#version-072","text":"cleaned up readme and help messages removed useless print statements improved CI tooling and checking minor bug fixes","title":"Version 0.7.2"},{"location":"changelog/#version-071","text":"split CI integration into separate platform specific files added new cli argument --cwd which changes the default save to location to the current working directory (this will be default in future) added unit tests to cover the new argument Changed license to a the more permissive Apache 2 software license","title":"Version 0.7.1"},{"location":"changelog/#version-070","text":"Fixed issues with logging to file. Finished adding tests for Unicode Support Deprecated some unneccessary code Clean up documentation and README removed config files no longer in use.","title":"Version 0.7.0"},{"location":"changelog/#version-0613","text":"Fixed bug that created a torrent file with no name. Fixed bug that would error if cli path was listed after announce urls Added full unicode support. Added Unittests for new features and bug fixes","title":"Version 0.6.13"},{"location":"changelog/#version-0611","text":"Fixed bug that occured during recheck when file of 0 length is included. Altered Recheck algorithm to process 0 length files. Only effected meta version 2 and hybrid torrent files. Added unittests to cover the situation.","title":"Version 0.6.11"},{"location":"changelog/#version-0610","text":"Updates to documentation Integrated Type hints in source code Updated build and CI process","title":"Version 0.6.10"},{"location":"changelog/#version-069","text":"The --progress flag is now --noprogress Default behavior is to show progress bar use --noprogress to not show added CLI Help format strings added custom CLI help formatter class Titled Help Message section headers Fixed a bunch of error pages created by mkdocs","title":"Version 0.6.9"},{"location":"changelog/#version-068","text":"Documentation for newest features CLI usage examples Improved unittests made progress bar active by default","title":"Version 0.6.8"},{"location":"changelog/#version-067","text":"Updates to API","title":"Version 0.6.7"},{"location":"changelog/#version-066","text":"bug that created faulty Bittorrent V2 meta files in some instances. back to working as it should.","title":"Version 0.6.6"},{"location":"changelog/#version-065","text":"Support for creating Magnet URI's Added optional progress bar for torrent creation Log File handler CLI args page in documentation verbose and logging bugs multi tracker errors bug","title":"Version 0.6.5"},{"location":"changelog/#version-064","text":"CLI interface add subcommands added interactive mode Re-wrote the recheck module fixed documentation and docstrings linting and testing errors","title":"Version 0.6.4"},{"location":"changelog/#version-063","text":"Fixed Bug that would format list of trackers incorrectly CLI Bug Fixes","title":"Version 0.6.3"},{"location":"changelog/#version-062","text":"Bug fixes Documentation error pages","title":"Version 0.6.2"},{"location":"changelog/#version-060","text":"cli commands alterations debug logging during creation process","title":"Version 0.6.0"},{"location":"changelog/#version-052","text":"Fixed Bug that was adding wrong fields to info dict","title":"Version 0.5.2"},{"location":"changelog/#version-050","text":"Slew of new unit tests Stricter linting features Alternative method of -re-check feature Bug Fixes CLI help formatting errors","title":"Version 0.5.0"},{"location":"changelog/#version-048","text":"Improved Algorithm performance for ReCheck. Additions to documentation.","title":"Version 0.4.8"},{"location":"changelog/#version-047","text":"Fixed A bug that misspelled a field when creating Hybrid torrent files. Re-Check procedure for v2 and hybrid torrent file checking.","title":"Version 0.4.7"},{"location":"changelog/#version-046","text":"CLI Help and Usage Messages. Expanded CLI args. Completely new CheckerClass which replaces old Checker Hooks for GUI or other 3rd party apps to hook into Checking Documentation and Unit tests for new CheckerClass","title":"Version 0.4.6"},{"location":"changelog/#version-045","text":"Documentation and docstrings improvements Better code formating and more detailed docstrings More unit tests.","title":"Version 0.4.5"},{"location":"changelog/#version-042","text":"The ReChecker feature now supports v1, v2, & hybrid .torrent file. Bug in CLI for python < 3.8","title":"Version 0.4.2"},{"location":"changelog/#version-041","text":"Added tests for hybrid class Added logging features new cli flag to activate debug mode Documentation theme. Fixed Bug that allowed improper piece length values.","title":"Version 0.4.1"},{"location":"changelog/#version-040","text":"Fixed bugs in creating hybrid files. Bug Fix that broke cli.","title":"Version 0.4.0"},{"location":"changelog/#version-030","text":"Added/Improved support for hybrid meta files. Many additions to testing suit including linting and coverage tests.","title":"Version 0.3.0"},{"location":"changelog/#version-028","text":"Styling fixes. Bug Fixes. Prelimenary support for bittorrent hybrid meta files. Bug Fixes","title":"Version 0.2.8"},{"location":"changelog/#version-027","text":"major imporvements to torrentfile-GUI. minor adjustments to this package for integration. Code consolidation Bug Fixes Documentation additions Implemented CI/CD Integration","title":"Version 0.2.7"},{"location":"changelog/#version-023","text":"Bug Fixes Code Style and Formatting Added more unittests","title":"Version 0.2.3"},{"location":"changelog/#version-021","text":"Bittorrent Protocol V2 Support v2 metafile options to cli v2 metafile tests","title":"Version 0.2.1"},{"location":"changelog/#version-017","text":"Docstrings Improvements. Added documentation rederer. Improved readme file. formatting","title":"Version 0.1.7"},{"location":"changelog/#version-012","text":"Added a Command Line Interface Rough Graphical User Interface Minor Bug Fixes Improved unittest coverage","title":"Version 0.1.2"},{"location":"changelog/#version-010","text":"added SHA256 support Feeder class for seemless file switching Fixed the primary entrypoint function. Improved docstrings Bug fixes","title":"Version 0.1.0"},{"location":"changelog/#version-002","text":"Added Unittests added bencode support added hashing support","title":"Version 0.0.2"},{"location":"changelog/#version-001","text":"Initial concept and planning","title":"Version 0.0.1"},{"location":"man/","text":"Name torrentfile Synopsis torrentfile [options] [options] Description torrentfile is a CLI tool for creating, editing, validating, or reviewing Bittorrent files(.torrent). It supports all current versions of the Bittorrent Protocol files as well as hybrid files. Has support for generating magnet URI's for meta files. Options -h displays all relevant command line options and subcommands. -V displays program and version. -v enables debug mode and outputs a large amount of information to the terminal. -i activates interactive mode for selecting subcommands and options. Sub-commands create alias: c , new torrentfile create [options] torrentfile c [options] The create subcommand is used for generating new torrent files. The only required argument is the path( ) to the contents file or directory. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. --piece-length Uses the number that follows as the piece-length size for the newly created torrent file. If option isn't used the program will pick the ideal size. Acceptable values include 14-29 which is interpreted as the number to raise 2 by (e.g. 14 is interpreted as 16384), or any perfect power of 2 greater than or equal to 16 KiB and less than 1 GiB. Example: --piece-length 14 or --piece-length 16384 . --meta-version Use the following number as the Bittorrent version the torrent will be used on. The default is 1. Options: 1 - Bittorrent v1, 2 - Bittorrent v2, 3 - Bittorrent v1 & v2 Examples: --meta-version 1 , --meta-version 2 , --meta-version 3 -o --out Specify the full path to the newly created torrent file. The default is to save it adjacent to the content. Example: if content is at /home/user/torrents/content the default would create /home/user/torrents/content.torrent --cwd --current Changes the default save location to the current working directory. Example: if content is at /home/user/torrents/content this option would create ./content.torrent --progress --prog Options (0, 1): No status bar will be shown if 0. Otherwise the default and 1 argument means progress bar is shown info alias: i torrentfile info torrentfile i There are no optional arguments for the info subcommand. /path/to/*.torrent The relative or absolute path to the torrent file. edit alias: e torrentfile edit [options] torrentfile e [options] Each option identifies the field to edit inside the torrent file and what the new value should be. If an option is not used then its field will be ommited in the newly created torrent file. As such if the file is marked as private and it should remain that way, the -p option should be used. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. recheck alias: r , check torrentfile recheck torrentfile r There are only two arguments for the recheck command and both are mandatory. The first is the absolute or relative to the torrent file, and the second is the absolute or relative to it's content. This will display a progress bar and at the end output what percent of the torrentfile's content it found at the path indicated. It is also permitted to use the contents parent directory as the second argument and the result will be the same.","title":"Man Page"},{"location":"man/#name","text":"torrentfile","title":"Name"},{"location":"man/#synopsis","text":"torrentfile [options] [options] ","title":"Synopsis"},{"location":"man/#description","text":"torrentfile is a CLI tool for creating, editing, validating, or reviewing Bittorrent files(.torrent). It supports all current versions of the Bittorrent Protocol files as well as hybrid files. Has support for generating magnet URI's for meta files.","title":"Description"},{"location":"man/#options","text":"-h displays all relevant command line options and subcommands. -V displays program and version. -v enables debug mode and outputs a large amount of information to the terminal. -i activates interactive mode for selecting subcommands and options.","title":"Options"},{"location":"man/#sub-commands","text":"","title":"Sub-commands"},{"location":"man/#create","text":"alias: c , new torrentfile create [options] torrentfile c [options] The create subcommand is used for generating new torrent files. The only required argument is the path( ) to the contents file or directory. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. --piece-length Uses the number that follows as the piece-length size for the newly created torrent file. If option isn't used the program will pick the ideal size. Acceptable values include 14-29 which is interpreted as the number to raise 2 by (e.g. 14 is interpreted as 16384), or any perfect power of 2 greater than or equal to 16 KiB and less than 1 GiB. Example: --piece-length 14 or --piece-length 16384 . --meta-version Use the following number as the Bittorrent version the torrent will be used on. The default is 1. Options: 1 - Bittorrent v1, 2 - Bittorrent v2, 3 - Bittorrent v1 & v2 Examples: --meta-version 1 , --meta-version 2 , --meta-version 3 -o --out Specify the full path to the newly created torrent file. The default is to save it adjacent to the content. Example: if content is at /home/user/torrents/content the default would create /home/user/torrents/content.torrent --cwd --current Changes the default save location to the current working directory. Example: if content is at /home/user/torrents/content this option would create ./content.torrent --progress --prog Options (0, 1): No status bar will be shown if 0. Otherwise the default and 1 argument means progress bar is shown","title":"create"},{"location":"man/#info","text":"alias: i torrentfile info torrentfile i There are no optional arguments for the info subcommand. /path/to/*.torrent The relative or absolute path to the torrent file.","title":"info"},{"location":"man/#edit","text":"alias: e torrentfile edit [options] torrentfile e [options] Each option identifies the field to edit inside the torrent file and what the new value should be. If an option is not used then its field will be ommited in the newly created torrent file. As such if the file is marked as private and it should remain that way, the -p option should be used. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT.","title":"edit"},{"location":"man/#recheck","text":"alias: r , check torrentfile recheck torrentfile r There are only two arguments for the recheck command and both are mandatory. The first is the absolute or relative to the torrent file, and the second is the absolute or relative to it's content. This will display a progress bar and at the end output what percent of the torrentfile's content it found at the path indicated. It is also permitted to use the contents parent directory as the second argument and the result will be the same.","title":"recheck"},{"location":"source/","text":"TorrentFile API and Source Torrent Module module torrentfile. torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. Classes MetaFile \u2014 Base Class for all TorrentFile classes. TorrentFile \u2014 Class for creating Bittorrent meta files. TorrentFileV2 \u2014 Class for creating Bittorrent meta v2 files. TorrentFileHybrid \u2014 Construct the Hybrid torrent meta file with provided parameters. TorrentAssembler \u2014 Assembler class for Bittorrent version 2 and hybrid meta files. ------ Utils Module module torrentfile. utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Classes Memo \u2014 Memoice chache object. MissingPathError \u2014 Path parameter is required to specify target content. PieceLengthValueError \u2014 Piece Length parameter must equal a perfect power of 2. Functions get_file_list ( path ) (list) \u2014 Return a sorted list of file paths contained in directory. get_piece_length ( size ) (int) \u2014 Calculate the ideal piece length for bittorrent data. humanize_bytes ( amount ) (str) \u2014 Convert integer into human readable memory sized denomination. next_power_2 ( value ) (int) \u2014 Calculate the next perfect power of 2 equal to or greater than value. normalize_piece_length ( piece_length ) (int) \u2014 Verify input piece_length is valid and convert accordingly. path_piece_length ( path ) (int) \u2014 Calculate piece length for input path and contents. path_size ( path ) (int) \u2014 Return the total size of all files in path recursively. path_stat ( path ) (list) \u2014 Calculate directory statistics. ------ Edit Module module torrentfile. edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds Functions edit_torrent ( metafile , args ) (dict) \u2014 Edit the properties and values in a torrent meta file. filter_empty ( args , meta , info ) \u2014 Remove dictionary keys with empty values. ------ Interactive Module module torrentfile. interactive Module contains the procedures used for Interactive Mode. Classes InteractiveEditor \u2014 Interactive dialog class for torrent editing. InteractiveCreator \u2014 Class namespace for interactive program options. Functions create_torrent ( ) \u2014 Create new torrent file interactively. edit_action ( ) \u2014 Edit the editable values of the torrent meta file. get_input ( *args ) (str) \u2014 Determine appropriate input function to call. recheck_torrent ( ) \u2014 Check torrent download completed percentage. select_action ( ) \u2014 Operate TorrentFile program interactively through terminal. showcenter ( txt ) \u2014 Print text to screen in the center position of the terminal. showtext ( txt ) \u2014 Print contents of txt to screen. ------ CLI Module module torrentfile. cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. Classes TorrentFileHelpFormatter \u2014 Formatting class for help tips provided by the CLI. Functions activate_logger ( ) \u2014 Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. main ( ) \u2014 Initiate main function for CLI script. ------ Recheck Module module torrentfile. recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Classes Checker \u2014 Check a given file or directory to see if it matches a torrentfile. FeedChecker \u2014 Validates torrent content. HashChecker \u2014 Verify that root hashes of content files match the .torrent files. ------ Hasher Module module torrentfile. hasher Piece/File Hashers for Bittorrent meta file contents. Classes Hasher \u2014 Piece hasher for Bittorrent V1 files. HasherV2 \u2014 Calculate the root hash and piece layers for file contents. HasherHybrid \u2014 Calculate root and piece hashes for creating hybrid torrent file. FileHasher \u2014 Calculate root and piece hashes for creating hybrid torrent file. Functions merkle_root ( blocks ) (bytes) \u2014 Calculate the merkle root for a seq of sha256 hash digests. ------ torrentfile Torrentfile can create Bittorrent metafiles for any content. Both Bittorrent v1 and v2 are fully supported. Also included is a torrent torrent file checker, which can verify a .torrent file is formated correctly as well as validate files and folders against metadata. Modules: metafile: Creation/Validation of v1 .torrent files. metafile2: Creation/Validation of v2 .torrent files. torrentfile: torrentfiles Command Line Interface implementation. exceptions: Custom Exceptions used in package. utils: Utilities used throughout package. create ( args ) Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args info ( args ) Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output magnet ( metafile ) Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile : str ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri __main__ Enable calling the package directly with python from the command line. main () Start the entry point script. Source code in torrentfile\\__main__.py 26 27 28 29 30 def main (): \"\"\" Start the entry point script. \"\"\" execute () cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. TorrentFileHelpFormatter Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts __init__ ( prog , width = 40 , max_help_positions = 30 ) Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) _format_text ( text ) Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" _join_parts ( part_strings ) Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) _split_lines ( text , _ ) Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] format_headers ( parts ) staticmethod Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts activate_logger () Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" ) execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args main () Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute () commands The commands module contains the Action Commands executed by the CLI script. Each function pertains to a command line action/subcommand and drives specific features of the application. Functions create_command info_command edit_command recheck_command magnet_command create ( args ) Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args edit ( args ) Execute the edit CLI sub-command with provided arguments. Parameters: Name Type Description Default args Namespace positional and optional CLI arguments. required Returns: Type Description str path to edited torrent file. Source code in torrentfile\\commands.py 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 def edit ( args : list ): \"\"\" Execute the edit CLI sub-command with provided arguments. Parameters ---------- args : Namespace positional and optional CLI arguments. Returns ------- str path to edited torrent file. \"\"\" metafile = args . metafile logger . info ( \"Editing %s Meta File\" , str ( args . metafile )) editargs = { \"url-list\" : args . url_list , \"httpseeds\" : args . httpseeds , \"announce\" : args . announce , \"source\" : args . source , \"private\" : args . private , \"comment\" : args . comment , } return edit_torrent ( metafile , editargs ) info ( args ) Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output magnet ( metafile ) Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile : str ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri recheck ( args ) Execute recheck CLI sub-command. Parameters: Name Type Description Default args Namespace positional and optional arguments. required Returns: Type Description str The percentage of content currently saved to disk. Source code in torrentfile\\commands.py 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 def recheck ( args : list ): \"\"\" Execute recheck CLI sub-command. Parameters ---------- args : Namespace positional and optional arguments. Returns ------- str The percentage of content currently saved to disk. \"\"\" metafile = args . metafile content = args . content logger . debug ( \"Validating %s against %s contents\" , metafile , content ) checker = Checker ( metafile , content ) logger . debug ( \"Completed initialization of the Checker class\" ) result = checker . results () sys . stdout . write ( str ( result ) + \"% Match \\n \" ) sys . stdout . flush () return result edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds edit_torrent ( metafile , args ) Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta filter_empty ( args , meta , info ) Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val ) hasher Piece/File Hashers for Bittorrent meta file contents. FileHasher Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () __init__ ( path , piece_length , progress = True , hybrid = False ) Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) __iter__ () Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self __next__ () Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash _calculate_root () Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] Hasher Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec __init__ ( paths , piece_length , progress = True ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) __iter__ () Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self __next__ () Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec _handle_partial ( arr ) Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec next_file () Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False HasherHybrid Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) _calculate_root () Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] process_file ( data ) Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () HasherV2 Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) _calculate_root () Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) process_file ( fd ) Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () merkle_root ( blocks ) Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks interactive Module contains the procedures used for Interactive Mode. InteractiveCreator Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () __init__ () Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () get_props () Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () InteractiveEditor Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) __init__ ( metafile ) Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } edit_props () Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) sanatize_response ( key , response ) Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val show_current () Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) _get_input ( txt ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value _get_input_loop ( txt , func ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" ) create_torrent () Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator edit_action () Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props () get_input ( * args ) Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- *args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args ) recheck_torrent () Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results select_action () Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover showcenter ( txt ) Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string ) showtext ( txt ) Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt ) mixins Collection of classes that can be used as Mixins with other base classes. Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the progress bar mixin. And any class is eligible to use the callback mixin. CbMixin Mixin class to set a callback during hashing procedure. Source code in torrentfile\\mixins.py 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class CbMixin : \"\"\" Mixin class to set a callback during hashing procedure. \"\"\" _cb = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover set_callback ( func ) classmethod Assign a callback to the Hashing class. Parameters: Name Type Description Default func function the callback function required Source code in torrentfile\\mixins.py 40 41 42 43 44 45 46 47 48 49 50 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover ProgMixin Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods prog_start prog_update prog_close Source code in torrentfile\\mixins.py 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 class ProgMixin : \"\"\" Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods ------- prog_start prog_update prog_close \"\"\" def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start ) def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush () def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False is_active () Test to see if there is an active progress bar for object. Returns: Name Type Description bool True if there is, otherwise False. Source code in torrentfile\\mixins.py 199 200 201 202 203 204 205 206 207 208 209 210 def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False prog_close () Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. Source code in torrentfile\\mixins.py 187 188 189 190 191 192 193 194 195 196 197 def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog prog_start ( total , path , length = 50 , unit = None ) Generate a new progress bar for the given file path. Parameters: Name Type Description Default total int the total amount of units accumulating towards. required path str path to file being hashed. required length int the number of characters of the actual progress bar. 50 unit str the text representation of the value being measured. None Source code in torrentfile\\mixins.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start ) prog_update ( val ) Update progress bar with given amount of progress. Parameters: Name Type Description Default val int the number of bytes count the progress bar should increase. required Source code in torrentfile\\mixins.py 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush () ProgressBar Holds the state and details of the terminal progress bars. Parameters: Name Type Description Default total int the total amount to be accumulated. required title str the subject of the progress tracker required length int the width of the progress bar required unit str the text representation incremented required Source code in torrentfile\\mixins.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 class ProgressBar : \"\"\" Holds the state and details of the terminal progress bars. Parameters ---------- total : int the total amount to be accumulated. title : str the subject of the progress tracker length : int the width of the progress bar unit : str the text representation incremented \"\"\" def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ]) def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar ) __init__ ( total , title , length , unit , start ) Construct the progress bar object and store state of it's properties. Source code in torrentfile\\mixins.py 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ]) increment ( value ) Increase the state of the progress bar value. Parameters: Name Type Description Default value int the amount to increment the state by. required Source code in torrentfile\\mixins.py 96 97 98 99 100 101 102 103 104 105 def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value pbar () Return the size of the filled portion of the progress bar. Returns: Name Type Description str the progress bar characters Source code in torrentfile\\mixins.py 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar ) waiting ( msg , flag , timeout = 180 ) Show loading message while thread completes processing. Parameters: Name Type Description Default msg str Message string printed before the progress bar required flag list Once flag is filled exit loop required timeout int max amount of time to run the function. 180 Source code in torrentfile\\mixins.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 def waiting ( msg , flag , timeout = 180 ): \"\"\" Show loading message while thread completes processing. Parameters ---------- msg : str Message string printed before the progress bar flag : list Once flag is filled exit loop timeout : int max amount of time to run the function. \"\"\" then = time . time () codes , fill = list ( range ( 9617 , 9620 )), chr ( 9619 ) size = idx = 0 total = shutil . get_terminal_size () . columns - len ( msg ) - 20 def output ( text ): \"\"\" Print parameter message to the console. Parameters ---------- text : str output message \"\"\" sys . stdout . write ( text ) sys . stdout . flush () output ( \" \\n \" ) while not flag : time . sleep ( 0.16 ) filled = ( fill * size ) + chr ( codes [ idx ]) + ( \" \" * ( total - size )) output ( f \" { msg } : { filled } \\r \" ) idx = idx + 1 if idx + 1 < len ( codes ) else 0 size = size + 1 if size < total else 0 if time . time () - then > timeout : break output ( \" \\n \" ) recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Checker Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required Example >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 __init__ ( metafile , path ) Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () check_paths () Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) find_root ( path ) Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) iter_hashes () Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 log_msg ( * args , level = logging . INFO ) Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) piece_checker () Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker register_callback ( hook ) classmethod Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook results () Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result walk_file_tree ( tree , partials ) Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) FeedChecker Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial __init__ ( checker ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 335 336 337 338 339 340 341 342 343 344 345 346 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 348 349 350 351 352 353 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self __next__ () Yield back result of comparison. Source code in torrentfile\\recheck.py 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) _gen_padding ( partial , length , read = 0 ) Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial extract ( path , partial ) Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad iter_pieces () Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () HashChecker Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required Source code in torrentfile\\recheck.py 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size Padder Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( length , piece_length ) Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 530 531 532 533 534 535 536 537 538 539 540 541 542 543 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () __iter__ () Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 545 546 547 548 549 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover __next__ () Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( checker ) Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 487 488 489 490 491 492 493 494 495 496 497 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 499 500 501 502 503 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self __next__ () Provide the result of comparison. Source code in torrentfile\\recheck.py 505 506 507 508 509 510 511 512 513 514 515 516 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr advance () Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size next_file () Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False process_current () Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. MetaFile Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta __init__ ( path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ ) Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name assemble () Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError set_callback ( func ) classmethod Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) sort_meta () Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta write ( outfile = None ) Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta TorrentAssembler Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFile Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- **kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces __init__ ( ** kwargs ) Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () assemble () Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces TorrentFileHybrid Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFileV2 Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree __init__ ( ** kwargs ) Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree assemble () Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Memo Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __call__ ( path ) Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __init__ ( func ) Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} MissingPathError Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) PieceLengthValueError Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) _filelist_total ( path ) Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist ) filelist_total ( pathstring ) Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError get_file_list ( path ) Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist get_piece_length ( size ) Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp humanize_bytes ( amount ) Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\" next_power_2 ( value ) Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start normalize_piece_length ( piece_length ) Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError path_piece_length ( path ) Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize ) path_size ( path ) Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size path_stat ( path ) Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length ) version Holds the release version number. torrentfile.cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. TorrentFileHelpFormatter Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts __init__ ( prog , width = 40 , max_help_positions = 30 ) Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) _format_text ( text ) Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" _join_parts ( part_strings ) Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) _split_lines ( text , _ ) Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] format_headers ( parts ) staticmethod Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts activate_logger () Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" ) execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args main () Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute () torrentfile.edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds edit_torrent ( metafile , args ) Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta filter_empty ( args , meta , info ) Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val ) torrentfile.hasher Piece/File Hashers for Bittorrent meta file contents. FileHasher Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () __init__ ( path , piece_length , progress = True , hybrid = False ) Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) __iter__ () Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self __next__ () Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash _calculate_root () Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] Hasher Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec __init__ ( paths , piece_length , progress = True ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) __iter__ () Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self __next__ () Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec _handle_partial ( arr ) Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec next_file () Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False HasherHybrid Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) _calculate_root () Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] process_file ( data ) Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () HasherV2 Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) _calculate_root () Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) process_file ( fd ) Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () merkle_root ( blocks ) Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks torrentfile.interactive Module contains the procedures used for Interactive Mode. InteractiveCreator Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () __init__ () Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () get_props () Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () InteractiveEditor Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) __init__ ( metafile ) Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } edit_props () Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) sanatize_response ( key , response ) Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val show_current () Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) _get_input ( txt ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value _get_input_loop ( txt , func ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" ) create_torrent () Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator edit_action () Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props () get_input ( * args ) Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- *args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args ) recheck_torrent () Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results select_action () Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover showcenter ( txt ) Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string ) showtext ( txt ) Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt ) torrentfile.recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Checker Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required Example >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 __init__ ( metafile , path ) Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () check_paths () Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) find_root ( path ) Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) iter_hashes () Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 log_msg ( * args , level = logging . INFO ) Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) piece_checker () Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker register_callback ( hook ) classmethod Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook results () Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result walk_file_tree ( tree , partials ) Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) FeedChecker Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial __init__ ( checker ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 335 336 337 338 339 340 341 342 343 344 345 346 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 348 349 350 351 352 353 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self __next__ () Yield back result of comparison. Source code in torrentfile\\recheck.py 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) _gen_padding ( partial , length , read = 0 ) Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial extract ( path , partial ) Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad iter_pieces () Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () HashChecker Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required Source code in torrentfile\\recheck.py 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size Padder Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( length , piece_length ) Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 530 531 532 533 534 535 536 537 538 539 540 541 542 543 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () __iter__ () Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 545 546 547 548 549 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover __next__ () Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( checker ) Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 487 488 489 490 491 492 493 494 495 496 497 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 499 500 501 502 503 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self __next__ () Provide the result of comparison. Source code in torrentfile\\recheck.py 505 506 507 508 509 510 511 512 513 514 515 516 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr advance () Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size next_file () Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False process_current () Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err torrentfile.torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. MetaFile Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta __init__ ( path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ ) Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name assemble () Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError set_callback ( func ) classmethod Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) sort_meta () Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta write ( outfile = None ) Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta TorrentAssembler Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFile Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- **kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces __init__ ( ** kwargs ) Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () assemble () Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces TorrentFileHybrid Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFileV2 Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree __init__ ( ** kwargs ) Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree assemble () Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers torrentfile.utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Memo Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __call__ ( path ) Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __init__ ( func ) Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} MissingPathError Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) PieceLengthValueError Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) _filelist_total ( path ) Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist ) filelist_total ( pathstring ) Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError get_file_list ( path ) Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist get_piece_length ( size ) Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp humanize_bytes ( amount ) Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\" next_power_2 ( value ) Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start normalize_piece_length ( piece_length ) Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError path_piece_length ( path ) Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize ) path_size ( path ) Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size path_stat ( path ) Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length ) torrentfile.version Holds the release version number. tests Unittest package init module. dir1 () Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 168 169 170 171 172 173 174 175 176 177 178 179 @pytest . fixture ( scope = \"package\" ) def dir1 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir () yield root rmpath ( root ) dir2 () Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 182 183 184 185 186 187 188 189 190 191 192 193 @pytest . fixture () def dir2 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir ( ext = \"2\" ) yield Path ( root ) rmpath ( root ) file1 () Return the path to a temporary file package scope. Source code in tests\\__init__.py 241 242 243 244 245 246 247 248 @pytest . fixture ( scope = \"package\" ) def file1 (): \"\"\" Return the path to a temporary file package scope. \"\"\" path = tempfile () yield path rmpath ( path ) file2 () Return the path to a temporary file no scope. Source code in tests\\__init__.py 297 298 299 300 301 302 303 304 @pytest . fixture () def file2 (): \"\"\" Return the path to a temporary file no scope. \"\"\" path = tempfile () yield path rmpath ( path ) filemeta1 ( file1 , request ) Test fixture for generating metafile for all versions of torrents. Source code in tests\\__init__.py 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 @pytest . fixture ( scope = \"package\" , params = torrents ()) def filemeta1 ( file1 , request ): \"\"\" Test fixture for generating metafile for all versions of torrents. \"\"\" args = { \"path\" : file1 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file1 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile ) filemeta2 ( file2 , request ) Test fixture for generating a meta file no scope. Source code in tests\\__init__.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 @pytest . fixture ( params = torrents ()) def filemeta2 ( file2 , request ): \"\"\" Test fixture for generating a meta file no scope. \"\"\" args = { \"path\" : file2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url7\" , \"url8\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file2 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile ) metafile1 ( dir1 , request ) Create a standard metafile for testing. Source code in tests\\__init__.py 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 @pytest . fixture ( scope = \"package\" , params = torrents ()) def metafile1 ( dir1 , request ): \"\"\" Create a standard metafile for testing. \"\"\" versions = torrents () args = { \"path\" : dir1 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir1 ) + str ( versions . index ( torrent_class )) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile ) metafile2 ( dir2 , request ) Create a standard metafile for testing. Source code in tests\\__init__.py 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 @pytest . fixture ( params = torrents ()) def metafile2 ( dir2 , request ): \"\"\" Create a standard metafile for testing. \"\"\" args = { \"path\" : dir2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"httpseeds\" : [ \"url6\" , \"url7\" ], \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir2 ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile ) rmpath ( * args ) Remove file or directory path. Parameters: Name Type Description Default *args list Filesystem locations for removing. () Source code in tests\\__init__.py 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def rmpath ( * args ): \"\"\"Remove file or directory path. Parameters ---------- *args : list Filesystem locations for removing. \"\"\" for arg in args : if not os . path . exists ( arg ): continue if os . path . isdir ( arg ): try : shutil . rmtree ( arg ) except PermissionError : # pragma: nocover pass elif os . path . isfile ( arg ): try : os . remove ( arg ) except PermissionError : # pragma: nocover pass sizedfiles ( dir2 , sizes , request ) Generate variable sized meta files for testing, no scope. Source code in tests\\__init__.py 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 @pytest . fixture ( params = torrents ()) def sizedfiles ( dir2 , sizes , request ): \"\"\" Generate variable sized meta files for testing, no scope. \"\"\" versions = torrents () args = { \"content\" : dir2 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , \"piece_length\" : sizes , } torrent_class = request . param version = str ( versions . index ( torrent_class )) outfile = str ( dir2 ) + version + str ( sizes ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile ) sizes ( request ) Generate powers of 2 for file creation package scope. Source code in tests\\__init__.py 159 160 161 162 163 164 165 @pytest . fixture ( scope = \"package\" , params = [ 2 ** i for i in range ( 15 , 20 )]) def sizes ( request ): \"\"\" Generate powers of 2 for file creation package scope. \"\"\" size = request . param yield size teardown () Remove all temporary directories and files. Source code in tests\\__init__.py 141 142 143 144 145 146 147 148 149 @atexit . register def teardown (): # pragma: nocover \"\"\" Remove all temporary directories and files. \"\"\" root = Path ( __file__ ) . parent / \"TESTDIR\" for path in [ root , \"./torrentfile.log\" ]: if os . path . exists ( path ): rmpath ( path ) tempdir ( ext = '1' ) Create temporary directory. Parameters: Name Type Description Default ext str , optional extension to file names, by default \"1\" '1' Returns: Type Description str path to common root for directory. Source code in tests\\__init__.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 def tempdir ( ext = \"1\" ): \"\"\"Create temporary directory. Parameters ---------- ext : str, optional extension to file names, by default \"1\" Returns ------- str path to common root for directory. \"\"\" layouts = { \"1\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.mp4\" , f \"dir { ext } /file3.mp3\" , f \"dir { ext } /file4.zip\" , f \"dir { ext } /file5.txt\" , f \"dir { ext } /subdir1/subdir2/file.7z\" , f \"dir { ext } /subdir/subdir/file4.rar\" , f \"dir { ext } /subdir/subdir/file4.r01\" , ], \"2\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.jpg\" , f \"dir { ext } /subdir/file2.mp4\" , f \"dir { ext } /subdir/file3.mp3\" , ], } paths = [] for path in layouts [ ext ]: temps = tempfile ( path = path , exp = 18 ) paths . append ( temps ) return os . path . commonpath ( paths ) tempfile ( path = None , exp = 18 ) Create temporary file. Creates a temporary file for unittesting purposes.py Parameters: Name Type Description Default path str , optional relative path to temporary files, by default None None exp int , optional Exponent used to determine size of file., by default 18 18 Returns: Type Description str absolute path to file. Source code in tests\\__init__.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 def tempfile ( path = None , exp = 18 ): \"\"\"Create temporary file. Creates a temporary file for unittesting purposes.py Parameters ---------- path : str, optional relative path to temporary files, by default None exp : int, optional Exponent used to determine size of file., by default 18 Returns ------- str absolute path to file. \"\"\" seq = ( string . printable + string . whitespace ) . encode ( \"utf-8\" ) root = Path ( __file__ ) . parent / \"TESTDIR\" if not os . path . exists ( root ): os . mkdir ( root ) if not path : path = root / ( str ( datetime . timestamp ( datetime . now ())) + \".file\" ) parts = Path ( path ) . parts partial = root for i , part in enumerate ( parts ): partial = partial / part if i == len ( parts ) - 1 : with open ( partial , \"wb\" ) as binfile : size = 2 ** exp while size > 0 : if len ( seq ) < size : binfile . write ( seq ) size -= len ( seq ) seq += seq else : binfile . write ( seq [: size ]) size -= size else : if not os . path . exists ( partial ): os . mkdir ( partial ) return partial torrents () Return seq of torrentfile objects. Source code in tests\\__init__.py 152 153 154 155 156 def torrents (): \"\"\" Return seq of torrentfile objects. \"\"\" return [ TorrentFile , TorrentFileV2 , TorrentFileHybrid , TorrentAssembler ] test_cli Testing functions for the command line interface. folder ( dir1 ) Yield a folder object as fixture. Source code in tests\\test_cli.py 41 42 43 44 45 46 47 48 49 @pytest . fixture ( scope = \"module\" ) def folder ( dir1 ): \"\"\" Yield a folder object as fixture. \"\"\" sfolder = str ( dir1 ) torrent = sfolder + \".torrent\" yield ( sfolder , torrent ) rmpath ( torrent ) test_cli_announce ( folder , piece_length , version ) Test announce cli flag. Source code in tests\\test_cli.py 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce ( folder , piece_length , version ): \"\"\" Test announce cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--tracker\" , \"https://announce.org/tracker\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"announce\" ] == \"https://announce.org/tracker\" test_cli_announce_list ( folder , version ) Test announce-list cli flag. Source code in tests\\test_cli.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce_list ( folder , version ): \"\"\" Test announce-list cli flag. \"\"\" folder , torrent = folder trackers = [ \"https://announce.org/tracker\" , \"https://announce.net/tracker\" , \"https://tracker.net/announce\" , ] args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , version , \"--tracker\" , ] + trackers sys . argv = args execute () meta = pyben . load ( torrent ) for url in trackers : assert url in [ j for i in meta [ \"announce-list\" ] for j in i ] test_cli_announce_path ( dir1 , flag ) Test CLI when path is placed after the trackers flag. Source code in tests\\test_cli.py 451 452 453 454 455 456 457 458 459 460 461 462 463 @pytest . mark . parametrize ( \"flag\" , [ \"-t\" , \"-w\" , \"--announce\" , \"--web-seed\" , \"--http-seed\" ] ) def test_cli_announce_path ( dir1 , flag ): \"\"\" Test CLI when path is placed after the trackers flag. \"\"\" args = [ \"torrentfile\" , \"create\" , flag , \"https://announce1.org\" , str ( dir1 )] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_comment ( folder , piece_length , version ) Test comment cli flag. Source code in tests\\test_cli.py 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_comment ( folder , piece_length , version ): \"\"\" Test comment cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--magnet\" , \"--comment\" , \"this is a comment\" , \"--progress\" , \"1\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"comment\" ] == \"this is a comment\" test_cli_created_by ( folder , piece_length , version ) Test if created torrents recieve a created by field in meta info. Source code in tests\\test_cli.py 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_created_by ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a created by field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"TorrentFile\" in meta [ \"created by\" ] test_cli_creation_date ( folder , piece_length , version ) Test if torrents created get an accurate timestamp. Source code in tests\\test_cli.py 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_creation_date ( folder , piece_length , version ): \"\"\" Test if torrents created get an accurate timestamp. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) num = float ( meta [ \"creation date\" ]) date = datetime . datetime . fromtimestamp ( num ) now = datetime . datetime . now () assert date . day == now . day assert date . year == now . year assert date . month == now . month test_cli_cwd ( folder ) Test outfile cli flag. Source code in tests\\test_cli.py 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 def test_cli_cwd ( folder ): \"\"\" Test outfile cli flag. \"\"\" folder , _ = folder args = [ \"torrentfile\" , \"create\" , \"--cwd\" , folder , ] sys . argv = args current = os . getcwd () name = os . path . basename ( folder ) outfile = os . path . join ( current , name ) + \".torrent\" execute () assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_empty_files ( dir2 , version , progress ) Test creating torrent with empty files. Source code in tests\\test_cli.py 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"progress\" , [ \"0\" , \"1\" ]) def test_cli_empty_files ( dir2 , version , progress ): \"\"\" Test creating torrent with empty files. \"\"\" args = [ \"torrentfile\" , \"create\" , str ( dir2 ), \"--meta-version\" , version , \"--source\" , \"somesource\" , \"--prog\" , progress , ] sys . argv = args def walk ( root , count ): \"\"\" Traverse directory to edit files. \"\"\" if root . is_file (): with open ( root , \"wb\" ) as _ : return 1 elif root . is_dir (): for item in root . iterdir (): if count >= 2 : break count += walk ( item , count ) return count walk ( dir2 , 0 ) execute () outfile = str ( dir2 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_help () Test showing help notice cli flag. Source code in tests\\test_cli.py 352 353 354 355 356 357 358 359 360 361 def test_cli_help (): \"\"\" Test showing help notice cli flag. \"\"\" args = [ \"-h\" ] sys . argv = args try : assert execute () except SystemExit : assert True test_cli_outfile ( dir1 , piece_length , version ) Test outfile cli flag. Source code in tests\\test_cli.py 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_outfile ( dir1 , piece_length , version ): \"\"\" Test outfile cli flag. \"\"\" outfile = dir1 + \"test.torrent\" args = [ \"torrentfile\" , \"create\" , dir1 , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-o\" , outfile , \"--prog\" , \"1\" , ] sys . argv = args execute () assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_piece_length ( folder , piece_length , version ) Test piece length cli flag. Source code in tests\\test_cli.py 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_piece_length ( folder , piece_length , version ): \"\"\" Test piece length cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--progress\" , \"0\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"piece length\" ] == piece_length test_cli_private ( folder ) Test private cli flag. Source code in tests\\test_cli.py 85 86 87 88 89 90 91 92 93 94 def test_cli_private ( folder ): \"\"\" Test private cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--private\" ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"private\" in meta [ \"info\" ] test_cli_slash_outpath ( dir1 , sep ) Test if output when outpath ends with a /. Source code in tests\\test_cli.py 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 @pytest . mark . parametrize ( \"sep\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_outpath ( dir1 , sep ): \"\"\" Test if output when outpath ends with a /. \"\"\" if sys . platform != \"win32\" : sep = \"/\" # pragma: nocover parent = os . path . dirname ( dir1 ) + sep args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , \"-o\" , parent , str ( dir1 ), ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_slash_path ( dir1 , ending ) Test if output when path ends with a /. Source code in tests\\test_cli.py 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 @pytest . mark . parametrize ( \"ending\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_path ( dir1 , ending ): \"\"\" Test if output when path ends with a /. \"\"\" if sys . platform != \"win32\" and ending == \" \\\\ \" : # pragma: nocover ending = \"/\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , str ( dir1 ) + ending , ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_v1 ( folder ) Basic create torrent cli command. Source code in tests\\test_cli.py 52 53 54 55 56 57 58 59 60 def test_cli_v1 ( folder ): \"\"\" Basic create torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_v2 ( folder ) Create torrent v2 cli command. Source code in tests\\test_cli.py 63 64 65 66 67 68 69 70 71 def test_cli_v2 ( folder ): \"\"\" Create torrent v2 cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"2\" ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_v3 ( folder ) Create hybrid torrent cli command. Source code in tests\\test_cli.py 74 75 76 77 78 79 80 81 82 def test_cli_v3 ( folder ): \"\"\" Create hybrid torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"3\" ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_web_seeds ( folder , piece_length , version ) Test if created torrents recieve a web seeds field in meta info. Source code in tests\\test_cli.py 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_web_seeds ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a web seeds field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-w\" , \"https://webseed.url/1\" , \"https://webseed.url/2\" , \"https://webseed.url/3\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"https://webseed.url/1\" in meta [ \"url-list\" ] test_cli_with_debug ( folder , piece_length , version ) Test debug mode cli flag. Source code in tests\\test_cli.py 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_debug ( folder , piece_length , version ): \"\"\" Test debug mode cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_with_source ( folder , piece_length , version ) Test source cli flag. Source code in tests\\test_cli.py 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_source ( folder , piece_length , version ): \"\"\" Test source cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--source\" , \"somesource\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"source\" ] == \"somesource\" test_fix () Test dir1 fixture is not None. Source code in tests\\test_cli.py 34 35 36 37 38 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and dir2 test_commands Testing functions for the sub-action commands from command line args. test_create_unicode_name ( file1 ) Test Unicode information in CLI args. Source code in tests\\test_commands.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 def test_create_unicode_name ( file1 ): \"\"\" Test Unicode information in CLI args. \"\"\" parent = os . path . dirname ( file1 ) filename = os . path . join ( parent , \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" ) args = [ \"torrentfile\" , \"-v\" , \"create\" , \"-a\" , \"tracker_url.com/announce_3456\" , \"tracker_url.net/announce_3456\" , \"--source\" , \"sourcetext\" , \"--comment\" , \"filename is \u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"-o\" , str ( filename ), str ( file1 ), ] sys . argv = args execute () assert os . path . exists ( filename ) test_fix () Test dir1 fixture is not None. Source code in tests\\test_commands.py 37 38 39 40 41 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and metafile1 and file1 and metafile2 and dir2 test_info ( field , file1 ) Test the info_command action from the Command Line Interface. Source code in tests\\test_commands.py 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 @pytest . mark . parametrize ( \"field\" , [ \"name\" , \"announce\" , \"source\" , \"comment\" , \"private\" , \"announce-list\" ], ) def test_info ( field , file1 ): \"\"\" Test the info_command action from the Command Line Interface. \"\"\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"url1\" , \"url2\" , \"url3\" , \"--web-seed\" , \"url4\" , \"url5\" , \"--http-seed\" , \"url6\" , \"url7\" , \"--private\" , \"--comment\" , \"ExampleComment\" , \"--source\" , \"examplesource\" , str ( file1 ), ] sys . argv = args execute () class Space : \"\"\" Stand in substitution for argparse.Namespace object. \"\"\" metafile = str ( file1 ) + \".torrent\" output = info ( Space ) assert field in output test_magnet ( metafile1 ) Test create magnet function scheme. Source code in tests\\test_commands.py 65 66 67 68 69 70 def test_magnet ( metafile1 ): \"\"\" Test create magnet function scheme. \"\"\" magnet_link = magnet ( metafile1 ) assert magnet_link . startswith ( \"magnet\" ) test_magnet_cli ( metafile1 ) Test magnet creation through CLI interface. Source code in tests\\test_commands.py 136 137 138 139 140 141 142 def test_magnet_cli ( metafile1 ): \"\"\" Test magnet creation through CLI interface. \"\"\" sys . argv [ 1 :] = [ \"m\" , str ( metafile1 )] uri = execute () assert \"magnet\" in uri test_magnet_empty () Test create magnet function scheme. Source code in tests\\test_commands.py 84 85 86 87 88 89 90 91 def test_magnet_empty (): \"\"\" Test create magnet function scheme. \"\"\" try : magnet ( \"file_that_does_not_exist\" ) except FileNotFoundError : assert True test_magnet_hex ( metafile1 ) Test create magnet function digest. Source code in tests\\test_commands.py 54 55 56 57 58 59 60 61 62 def test_magnet_hex ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) info = meta [ \"info\" ] binfo = sha1 ( pyben . dumps ( info )) . hexdigest () . upper () assert binfo in magnet_link test_magnet_no_announce_list ( metafile2 ) Test create magnet function scheme. Source code in tests\\test_commands.py 73 74 75 76 77 78 79 80 81 def test_magnet_no_announce_list ( metafile2 ): \"\"\" Test create magnet function scheme. \"\"\" meta = pyben . load ( metafile2 ) del meta [ \"announce-list\" ] pyben . dump ( meta , metafile2 ) magnet_link = magnet ( metafile2 ) assert magnet_link . startswith ( \"magnet\" ) test_magnet_uri ( metafile1 ) Test create magnet function digest. Source code in tests\\test_commands.py 44 45 46 47 48 49 50 51 def test_magnet_uri ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) announce = meta [ \"announce\" ] assert quote_plus ( announce ) in magnet_link test_merkle_root_no_blocks ( blocks ) Test running merkle root function with 1 and 0 len lists. Source code in tests\\test_commands.py 171 172 173 174 175 176 177 178 179 @pytest . mark . parametrize ( \"blocks\" , [[], [ sha1 ( b \"1010\" ) . digest ()]]) # nosec def test_merkle_root_no_blocks ( blocks ): \"\"\" Test running merkle root function with 1 and 0 len lists. \"\"\" if blocks : assert merkle_root ( blocks ) else : assert not merkle_root ( blocks ) test_mixins_progbar ( torrent ) Test progbar mixins with small file. Source code in tests\\test_commands.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 @pytest . mark . parametrize ( \"torrent\" , torrents ()) def test_mixins_progbar ( torrent ): \"\"\" Test progbar mixins with small file. \"\"\" tfile = tempfile ( exp = 14 ) msg = \"1234abcd\" * 80 with open ( tfile , \"wb\" ) as temp : temp . write ( msg . encode ( \"utf-8\" )) args = { \"path\" : tfile , \"--prog\" : \"1\" , } metafile = torrent ( ** args ) output , _ = metafile . write () assert output == str ( tfile ) + \".torrent\" rmpath ( tfile ) test_edit Testing the edit torrent feature. test_edit_cli ( metafile2 , comment , source , announce , webseed , httpseed ) Test edit torrent with all params on cli. Source code in tests\\test_edit.py 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 @pytest . mark . parametrize ( \"comment\" , [ \"commenta\" , \"commentb\" , \"commentc\" ]) @pytest . mark . parametrize ( \"source\" , [ \"sourcea\" , \"sourceb\" , \"sourcec\" ]) @pytest . mark . parametrize ( \"announce\" , [[ \"url1\" , \"url2\" , \"url3\" ], [ \"url1\" ]]) @pytest . mark . parametrize ( \"webseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) @pytest . mark . parametrize ( \"httpseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) def test_edit_cli ( metafile2 , comment , source , announce , webseed , httpseed ): \"\"\" Test edit torrent with all params on cli. \"\"\" sys . argv = [ \"torrentfile\" , \"edit\" , metafile2 , \"--comment\" , comment , \"--source\" , source , \"--web-seed\" , webseed , \"--http-seed\" , httpseed , \"--tracker\" , announce , \"--private\" , ] main () meta = pyben . load ( metafile2 ) info = meta [ \"info\" ] assert comment == info . get ( \"comment\" ) assert source == info . get ( \"source\" ) assert info . get ( \"private\" ) == 1 assert meta [ \"announce-list\" ] == [[ announce ]] assert meta [ \"url-list\" ] == [ webseed ] test_edit_comment ( metafile2 , comment ) Test edit torrent with comment param. Source code in tests\\test_edit.py 118 119 120 121 122 123 124 125 126 127 @pytest . mark . parametrize ( \"comment\" , [ \"COMMENT\" , \"COMIT\" , \"MITCO\" ]) def test_edit_comment ( metafile2 , comment ): \"\"\" Test edit torrent with comment param. \"\"\" edits = { \"comment\" : comment } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"comment\" ] == comment test_edit_httpseeds ( metafile2 , httpseed ) Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 104 105 106 107 108 109 110 111 112 113 114 115 @pytest . mark . parametrize ( \"httpseed\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_httpseeds ( metafile2 , httpseed ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"httpseeds\" : httpseed } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseed test_edit_httpseeds_str ( metafile2 , httpseeds ) Test edit torrent with webseed param. Source code in tests\\test_edit.py 78 79 80 81 82 83 84 85 86 87 @pytest . mark . parametrize ( \"httpseeds\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_httpseeds_str ( metafile2 , httpseeds ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"httpseeds\" : httpseeds } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseeds . split () test_edit_none ( metafile2 ) Test edit torrent with None for all params. Source code in tests\\test_edit.py 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 def test_edit_none ( metafile2 ): \"\"\" Test edit torrent with None for all params. \"\"\" edits = { \"announce\" : None , \"url-list\" : None , \"comment\" : None , \"source\" : None , \"private\" : None , } data = pyben . load ( metafile2 ) edited = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta == edited test_edit_private_false ( metafile2 ) Test edit torrent with private param False. Source code in tests\\test_edit.py 153 154 155 156 157 158 159 160 161 def test_edit_private_false ( metafile2 ): \"\"\" Test edit torrent with private param False. \"\"\" edits = { \"private\" : \"\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert \"private\" not in data [ \"info\" ] test_edit_private_true ( metafile2 ) Test edit torrent with private param. Source code in tests\\test_edit.py 142 143 144 145 146 147 148 149 150 def test_edit_private_true ( metafile2 ): \"\"\" Test edit torrent with private param. \"\"\" edits = { \"private\" : \"1\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"private\" ] == 1 test_edit_removal ( metafile2 ) Test edit torrent with empty for all params. Source code in tests\\test_edit.py 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_edit_removal ( metafile2 ): \"\"\" Test edit torrent with empty for all params. \"\"\" edits = { \"announce\" : \"\" , \"url-list\" : \"\" , \"httpseeds\" : \"\" , \"comment\" : \"\" , \"source\" : \"\" , \"private\" : \"\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta test_edit_source ( metafile2 , source ) Test edit torrent with source param. Source code in tests\\test_edit.py 130 131 132 133 134 135 136 137 138 139 @pytest . mark . parametrize ( \"source\" , [ \"SomeSource\" , \"NoSouce\" , \"MidSource\" ]) def test_edit_source ( metafile2 , source ): \"\"\" Test edit torrent with source param. \"\"\" edits = { \"source\" : source } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"source\" ] == source test_edit_torrent ( metafile2 , announce ) Test edit torrent with announce param. Source code in tests\\test_edit.py 40 41 42 43 44 45 46 47 48 49 50 51 @pytest . mark . parametrize ( \"announce\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_torrent ( metafile2 , announce ): \"\"\" Test edit torrent with announce param. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce ] test_edit_torrent_str ( metafile2 , announce ) Test edit torrent with announce param as string. Source code in tests\\test_edit.py 54 55 56 57 58 59 60 61 62 63 @pytest . mark . parametrize ( \"announce\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_torrent_str ( metafile2 , announce ): \"\"\" Test edit torrent with announce param as string. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce . split ()] test_edit_urllist ( metafile2 , url_list ) Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 90 91 92 93 94 95 96 97 98 99 100 101 @pytest . mark . parametrize ( \"url_list\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_urllist ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list test_edit_urllist_str ( metafile2 , url_list ) Test edit torrent with webseed param. Source code in tests\\test_edit.py 66 67 68 69 70 71 72 73 74 75 @pytest . mark . parametrize ( \"url_list\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_urllist_str ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list . split () test_fix () Testing dir fixtures. Source code in tests\\test_edit.py 33 34 35 36 37 def test_fix (): \"\"\" Testing dir fixtures. \"\"\" assert dir2 and metafile2 and dir1 test_metafile_edit_with_unicode ( metafile2 ) Test if editing full unicode works as it should. Source code in tests\\test_edit.py 233 234 235 236 237 238 239 240 241 242 243 244 245 246 def test_metafile_edit_with_unicode ( metafile2 ): \"\"\" Test if editing full unicode works as it should. \"\"\" edits = { \"comment\" : \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"source\" : \"\u4e02\u4e03\u4e07\u4e0f\u4e11\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) com1 = data [ \"info\" ][ \"comment\" ] com2 = meta [ \"info\" ][ \"comment\" ] msg = edits [ \"comment\" ] assert com1 == com2 == msg test_interactive Testing functions for the command line interface. test_fixtures () Test the fixtures used in module. Source code in tests\\test_interactive.py 37 38 39 40 41 def test_fixtures (): \"\"\" Test the fixtures used in module. \"\"\" assert filemeta2 and file1 and file2 test_inter_create_full ( file1 , piece_length , announce , comment , source , url_list , version , monkeypatch ) Test creating torrent interactively with many parameters. Source code in tests\\test_interactive.py 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"piece_length\" , [ \"23\" , \"18\" , \"131072\" ]) @pytest . mark . parametrize ( \"announce\" , [ \"url1\" , \"urla urlb urlc\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Do\" , \"Ra\" , \"Me\" ]) def test_inter_create_full ( file1 , piece_length , announce , comment , source , url_list , version , monkeypatch , ): \"\"\" Test creating torrent interactively with many parameters. \"\"\" mapping = [ \"create\" , piece_length , announce , url_list , url_list , comment , source , \"Y\" , file1 , str ( file1 ) + \".torrent\" , version , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta = pyben . load ( str ( file1 ) + \".torrent\" ) assert meta [ \"info\" ][ \"source\" ] == source assert meta [ \"info\" ][ \"piece length\" ] == normalize_piece_length ( piece_length ) assert meta [ \"info\" ][ \"comment\" ] == comment assert meta [ \"url-list\" ] == url_list . split () test_inter_edit_cli ( filemeta2 , announce , cmnt , srce , urllist , monkeypatch ) Test editing torrent interactively from CLI. Source code in tests\\test_interactive.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 @pytest . mark . parametrize ( \"announce\" , [ \"urla urlb urlc\" , \"urld url2\" ]) @pytest . mark . parametrize ( \"urllist\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"cmnt\" , [ \"Some Comment\" ]) @pytest . mark . parametrize ( \"srce\" , [ \"Do\" , \"Ra\" ]) def test_inter_edit_cli ( filemeta2 , announce , cmnt , srce , urllist , monkeypatch ): \"\"\" Test editing torrent interactively from CLI. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , cmnt , \"2\" , srce , \"5\" , urllist , urllist , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) sys . argv = [ \"torrentfile\" , \"-i\" ] main () meta2 = pyben . load ( filemeta2 ) assert meta2 [ \"info\" ][ \"source\" ] == srce assert meta2 [ \"info\" ][ \"comment\" ] == cmnt assert meta2 [ \"url-list\" ] == urllist . split () assert meta2 [ \"info\" ][ \"private\" ] == 1 test_inter_edit_full ( filemeta2 , announce , comment , source , url_list , monkeypatch ) Test editing torrent file interactively. Source code in tests\\test_interactive.py 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 @pytest . mark . parametrize ( \"announce\" , [ \"url1\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Fa\" , \"So\" , \"La\" ]) def test_inter_edit_full ( filemeta2 , announce , comment , source , url_list , monkeypatch ): \"\"\" Test editing torrent file interactively. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , comment , \"2\" , source , \"5\" , url_list , \"\" , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta1 = pyben . load ( filemeta2 ) assert meta1 [ \"info\" ][ \"source\" ] == source assert meta1 [ \"info\" ][ \"comment\" ] == comment assert meta1 [ \"url-list\" ] == url_list . split () assert meta1 [ \"info\" ][ \"private\" ] == 1 test_inter_recheck ( torrentclass , monkeypatch , file1 ) Test interactive recheck function. Source code in tests\\test_interactive.py 180 181 182 183 184 185 186 187 188 189 190 191 @pytest . mark . parametrize ( \"torrentclass\" , torrents ()) def test_inter_recheck ( torrentclass , monkeypatch , file1 ): \"\"\" Test interactive recheck function. \"\"\" torrent = torrentclass ( path = file1 ) filemeta , _ = torrent . write () seq = [ \"recheck\" , filemeta , str ( file1 )] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) result = select_action () assert result == 100 test_interactive_create ( monkeypatch , file1 ) Test creating torrent interactively. Source code in tests\\test_interactive.py 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 def test_interactive_create ( monkeypatch , file1 ): \"\"\" Test creating torrent interactively. \"\"\" mapping = [ \"create\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , file1 , str ( file1 ) + \".torrent\" , \"\" , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () assert os . path . exists ( str ( file1 ) + \".torrent\" ) test_recheck Testing functions for the progress module. test_checker_callback ( dir1 , metafile1 ) Test Checker class with directory that points to nothing. Source code in tests\\test_recheck.py 125 126 127 128 129 130 131 def test_checker_callback ( dir1 , metafile1 ): \"\"\" Test Checker class with directory that points to nothing. \"\"\" Checker . register_callback ( lambda * x : print ( x )) checker = Checker ( metafile1 , str ( dir1 )) assert checker . results () == 100 test_checker_class ( dir1 , metafile1 ) Test Checker Class against meta files. Source code in tests\\test_recheck.py 42 43 44 45 46 47 def test_checker_class ( dir1 , metafile1 ): \"\"\" Test Checker Class against meta files. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100 test_checker_class_allfiles ( sizedfiles , dir2 ) Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 def test_checker_class_allfiles ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" def traverse ( path ): \"\"\" Traverse internal subdirectories. \"\"\" if path . is_file (): rmpath ( path ) elif path . is_dir (): for item in path . iterdir (): traverse ( item ) traverse ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_class_allpaths ( sizedfiles , dir2 ) Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 211 212 213 214 215 216 217 218 def test_checker_class_allpaths ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" for item in Path ( str ( dir2 )) . iterdir (): rmpath ( item ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_class_half_file ( filemeta2 , file2 ) Test Checker class with half size single file. Source code in tests\\test_recheck.py 221 222 223 224 225 226 227 228 229 230 231 232 def test_checker_class_half_file ( filemeta2 , file2 ): \"\"\" Test Checker class with half size single file. \"\"\" half = int ( os . path . getsize ( file2 ) / 2 ) barr = bytearray ( half ) with open ( file2 , \"rb\" ) as content : content . readinto ( barr ) with open ( file2 , \"wb\" ) as content : content . write ( barr ) checker = Checker ( filemeta2 , file2 ) assert int ( checker . results ()) != 10 test_checker_cli_args ( dir1 , metafile1 ) Test exclusive Checker Mode CLI. Source code in tests\\test_recheck.py 134 135 136 137 138 139 140 def test_checker_cli_args ( dir1 , metafile1 ): \"\"\" Test exclusive Checker Mode CLI. \"\"\" sys . argv = [ \"torrentfile\" , \"check\" , str ( metafile1 ), str ( dir1 )] output = main () assert output == 100 test_checker_empty_files ( dir2 , sizedfiles ) Test Checker when directory contains 0 length files. Source code in tests\\test_recheck.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 def test_checker_empty_files ( dir2 , sizedfiles ): \"\"\" Test Checker when directory contains 0 length files. \"\"\" def empty_files ( root ): \"\"\" Dump contents of files. \"\"\" if os . path . isfile ( root ): with open ( root , \"wb\" ) as _ : pass assert os . path . getsize ( root ) == 0 elif os . path . isdir ( root ): for item in os . listdir ( root ): return empty_files ( os . path . join ( root , item )) return root empty_files ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100 test_checker_first_piece ( dir2 , sizedfiles ) Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 def test_checker_first_piece ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if path . is_file (): new = b \"Something other than what was there before.\" with open ( path , \"rb\" ) as bfile : data = bfile . read () new_len = len ( new ) content = b \"\" . join ([ new , data [ new_len :]]) with open ( path , \"wb\" ) as bdoc : bdoc . write ( content ) elif path . is_dir (): for item in path . iterdir (): change ( item ) change ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100 test_checker_first_piece_alt ( dir2 , sizedfiles ) Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 def test_checker_first_piece_alt ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if os . path . isfile ( path ): with open ( path , \"rb\" ) as bfile : data = bfile . read () new = b \"some_other_bytes_to_use\" new_len = len ( new ) with open ( path , \"wb\" ) as wfile : wfile . write ( new + data [ new_len :]) elif os . path . isdir ( path ): for item in os . listdir ( path ): change ( os . path . join ( path , item )) change ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100 test_checker_missing ( sizedfiles , dir2 ) Test Checker class when files are missing from contents. Source code in tests\\test_recheck.py 179 180 181 182 183 184 185 186 187 188 def test_checker_missing ( sizedfiles , dir2 ): \"\"\" Test Checker class when files are missing from contents. \"\"\" count = 0 for fd in Path ( dir2 ) . iterdir (): if fd . is_file () and count < 2 : rmpath ( fd ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_missing_singles ( dir2 , sizedfiles ) Test Checker class with half size single file. Source code in tests\\test_recheck.py 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def test_checker_missing_singles ( dir2 , sizedfiles ): \"\"\" Test Checker class with half size single file. \"\"\" def walk ( root ): \"\"\" Remove first file found. \"\"\" if root . is_file (): rmpath ( root ) return True if root . is_dir (): for item in root . iterdir (): walk ( item ) return False walk ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_no_meta_file () Test Checker when incorrect metafile is provided. Source code in tests\\test_recheck.py 159 160 161 162 163 164 165 166 def test_checker_no_meta_file (): \"\"\" Test Checker when incorrect metafile is provided. \"\"\" try : Checker ( \"peaches\" , \"$\" ) except FileNotFoundError : assert True test_checker_parent_dir ( dir1 , metafile1 ) Test providing the parent directory for torrent checking feature. Source code in tests\\test_recheck.py 143 144 145 146 147 148 def test_checker_parent_dir ( dir1 , metafile1 ): \"\"\" Test providing the parent directory for torrent checking feature. \"\"\" checker = Checker ( metafile1 , os . path . dirname ( dir1 )) assert checker . results () == 100 test_checker_result_property ( dir1 , metafile1 ) Test Checker class with half size single file. Source code in tests\\test_recheck.py 257 258 259 260 261 262 263 def test_checker_result_property ( dir1 , metafile1 ): \"\"\" Test Checker class with half size single file. \"\"\" checker = Checker ( metafile1 , dir1 ) result = checker . results () assert checker . results () == result test_checker_simplest ( dir1 , metafile1 ) Test the simplest example. Source code in tests\\test_recheck.py 266 267 268 269 270 271 def test_checker_simplest ( dir1 , metafile1 ): \"\"\" Test the simplest example. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100 test_checker_with_file ( file1 , filemeta1 ) Test checker with single file torrent. Source code in tests\\test_recheck.py 151 152 153 154 155 156 def test_checker_with_file ( file1 , filemeta1 ): \"\"\" Test checker with single file torrent. \"\"\" checker = Checker ( filemeta1 , file1 ) assert checker . results () == 100 test_checker_wrong_root_dir ( metafile1 ) Test Checker when incorrect root directory is provided. Source code in tests\\test_recheck.py 169 170 171 172 173 174 175 176 def test_checker_wrong_root_dir ( metafile1 ): \"\"\" Test Checker when incorrect root directory is provided. \"\"\" try : Checker ( metafile1 , \"fake\" ) except FileNotFoundError : assert True test_fixtures () Test fixtures exist. Source code in tests\\test_recheck.py 33 34 35 36 37 38 39 def test_fixtures (): \"\"\" Test fixtures exist. \"\"\" assert dir1 and dir2 and file1 and file2 assert filemeta1 and filemeta2 and metafile1 assert metafile2 and sizes and sizedfiles test_partial_metafiles ( dir2 , sizedfiles ) Test Checker with data that is expected to be incomplete. Source code in tests\\test_recheck.py 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 def test_partial_metafiles ( dir2 , sizedfiles ): \"\"\" Test Checker with data that is expected to be incomplete. \"\"\" def shortenfile ( path ): \"\"\" Shorten a few files for testing purposes. \"\"\" with open ( path , \"rb\" ) as bfile : data = bfile . read () with open ( path , \"wb\" ) as bfile : bfile . write ( data [: - ( 2 ** 10 )]) for item in os . listdir ( dir2 ): full = os . path . join ( dir2 , item ) if os . path . isfile ( full ): shortenfile ( full ) testdir = os . path . dirname ( dir2 ) checker = Checker ( sizedfiles , testdir ) assert checker . results () != 100 test_recheck_wrong_dir ( metafile1 ) Test recheck function with directory that doesn't contain the contents. Source code in tests\\test_recheck.py 297 298 299 300 301 302 303 304 305 def test_recheck_wrong_dir ( metafile1 ): \"\"\" Test recheck function with directory that doesn't contain the contents. \"\"\" grandparent = os . path . dirname ( os . path . dirname ( metafile1 )) try : _ = Checker ( metafile1 , grandparent ) except FileNotFoundError : assert True test_torrent Testing functions for the torrent module. test_create_cwd_fail () Test cwd argument with create command failure. Source code in tests\\test_torrent.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_create_cwd_fail (): \"\"\"Test cwd argument with create command failure.\"\"\" class SuFile : \"\"\"A mock admin file.\"\"\" @staticmethod def __fspath__ (): raise PermissionError def __str__ ( self ): return \"SuFile\" tfile = tempfile () name = os . path . basename ( tfile ) + \".torrent\" torrent = MetaFile ( path = tfile ) sufile = SuFile () torrent . write ( outfile = sufile ) current = os . path . join ( \".\" , name ) assert os . path . exists ( current ) rmpath ( tfile , current ) test_fixtures () Test pytest fixtures. Source code in tests\\test_torrent.py 32 33 34 35 36 def test_fixtures (): \"\"\" Test pytest fixtures. \"\"\" assert dir1 and dir2 test_mbtorrent ( version , progress ) Test torrent creation for file size larger than 10MB. Source code in tests\\test_torrent.py 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 @pytest . mark . parametrize ( \"version\" , torrents ()) @pytest . mark . parametrize ( \"progress\" , [ 0 , 1 , 2 ]) def test_mbtorrent ( version , progress ): \"\"\" Test torrent creation for file size larger than 10MB. \"\"\" tfile = tempfile ( exp = 26 ) args = { \"path\" : tfile , \"progress\" : progress , \"piece_length\" : \"14\" , } torrent = version ( ** args ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile ) test_metafile_assemble ( dir1 ) Test assembling base metafile exception. Source code in tests\\test_torrent.py 50 51 52 53 54 55 56 57 58 def test_metafile_assemble ( dir1 ): \"\"\" Test assembling base metafile exception. \"\"\" metafile = MetaFile ( path = dir1 ) try : metafile . assemble () except NotImplementedError : assert True test_torrentfile_extra ( dir2 , version ) Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_extra ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" def walk ( item ): \"\"\" Edit files in directory structure. \"\"\" if item . is_file (): with open ( item , \"ab\" ) as binfile : binfile . write ( bytes ( 1000 )) elif item . is_dir (): for sub in item . iterdir (): walk ( sub ) walk ( dir2 ) args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\" test_torrentfile_missing_path ( version ) Test missing path error exception. Source code in tests\\test_torrent.py 39 40 41 42 43 44 45 46 47 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_missing_path ( version ): \"\"\" Test missing path error exception. \"\"\" try : version () except MissingPathError : assert True test_torrentfile_one_empty ( dir2 , version ) Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_one_empty ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" a = next ( os . walk ( dir2 )) if len ( a [ - 1 ]) > 0 : with open ( os . path . join ( a [ 0 ], a [ - 1 ][ 0 ]), \"w\" , encoding = \"utf-8\" ) as _ : pass args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\" test_torrentfile_single ( version , num , piece_length , capsys ) Test creating a torrent file from a single file contents. Source code in tests\\test_torrent.py 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 @pytest . mark . parametrize ( \"num\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single ( version , num , piece_length , capsys ): \"\"\" Test creating a torrent file from a single file contents. \"\"\" tfile = tempfile ( exp = num ) with capsys . disabled (): version . set_callback ( print ) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } trent = version ( ** args ) trent . write () assert os . path . exists ( str ( tfile ) + \".torrent\" ) rmpath ( tfile , str ( tfile ) + \".torrent\" ) test_torrentfile_single_extra ( version , size , piece_length ) Test creating a torrent file from a single file contents plus extra. Source code in tests\\test_torrent.py 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 @pytest . mark . parametrize ( \"size\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single_extra ( version , size , piece_length ): \"\"\" Test creating a torrent file from a single file contents plus extra. \"\"\" tfile = tempfile ( exp = size ) with open ( tfile , \"ab\" ) as binfile : binfile . write ( bytes ( str ( tfile ) . encode ( \"utf-8\" ))) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } torrent = version ( ** args ) torrent . write () outfile = str ( tfile ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( tfile , outfile ) test_torrentfile_single_under ( ver , sze , piecelength ) Test creating a torrent file from less than a single file contents. Source code in tests\\test_torrent.py 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 @pytest . mark . parametrize ( \"sze\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piecelength\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"ver\" , torrents ()) def test_torrentfile_single_under ( ver , sze , piecelength ): \"\"\" Test creating a torrent file from less than a single file contents. \"\"\" tfile = tempfile ( exp = sze ) with open ( tfile , \"rb\" ) as binfile : data = binfile . read () with open ( tfile , \"wb\" ) as binfile : binfile . write ( data [: - ( 2 ** 9 )]) kwargs = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piecelength , } torrent = ver ( ** kwargs ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile ) test_waiting_mixin () Test waiting function. Source code in tests\\test_torrent.py 198 199 200 201 202 203 204 205 206 def test_waiting_mixin (): \"\"\" Test waiting function. \"\"\" msg = \"Testing message\" lst = [] timeout = 3 waiting ( msg , lst , timeout = timeout ) assert len ( lst ) == 0 test_utils Unittest functions for testing torrentfile utils module. test_filelist_total ( dir1 ) Test function for acquiring a filelist for directory. Source code in tests\\test_utils.py 118 119 120 121 122 123 def test_filelist_total ( dir1 ): \"\"\" Test function for acquiring a filelist for directory. \"\"\" total , _ = utils . filelist_total ( dir1 ) assert total == ( 2 ** 18 ) * 8 test_filelisttotal_missing ( dir2 ) Test function filelist total with missing path. Parameters: Name Type Description Default dir2 pytest . fixture fixture containing a temporary directory required Source code in tests\\test_utils.py 225 226 227 228 229 230 231 232 233 234 235 236 237 def test_filelisttotal_missing ( dir2 ): \"\"\"Test function filelist total with missing path. Parameters ---------- dir2 : pytest.fixture fixture containing a temporary directory \"\"\" rmpath ( dir2 ) try : utils . filelist_total ( dir2 ) except utils . MissingPathError : assert True test_get_filelist ( dir1 ) Test function for get a list of files in a directory. Source code in tests\\test_utils.py 102 103 104 105 106 107 def test_get_filelist ( dir1 ): \"\"\" Test function for get a list of files in a directory. \"\"\" filelist = utils . get_file_list ( dir1 ) assert len ( filelist ) == 8 test_get_path_length_max ( dir1 ) Test function for getting piece length for folders max. Source code in tests\\test_utils.py 71 72 73 74 75 def test_get_path_length_max ( dir1 ): \"\"\" Test function for getting piece length for folders max. \"\"\" assert utils . path_piece_length ( dir1 ) <= ( 2 ** 27 ) test_get_path_length_min ( dir1 ) Test function for getting piece length for folders min. Source code in tests\\test_utils.py 64 65 66 67 68 def test_get_path_length_min ( dir1 ): \"\"\" Test function for getting piece length for folders min. \"\"\" assert utils . path_piece_length ( dir1 ) >= ( 2 ** 14 ) test_get_path_length_mod ( dir1 ) Test function for the best piece length for provided path. Source code in tests\\test_utils.py 57 58 59 60 61 def test_get_path_length_mod ( dir1 ): \"\"\" Test function for the best piece length for provided path. \"\"\" assert utils . path_piece_length ( dir1 ) % ( 2 ** 14 ) == 0 test_get_path_size ( dir1 ) Test function for getting total size of directory. Source code in tests\\test_utils.py 110 111 112 113 114 115 def test_get_path_size ( dir1 ): \"\"\" Test function for getting total size of directory. \"\"\" pathsize = utils . path_size ( dir1 ) assert pathsize == ( 2 ** 18 ) * 8 test_get_piece_length ( size ) Test function for best piece length for given size. Source code in tests\\test_utils.py 30 31 32 33 34 35 36 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length ( size ): \"\"\" Test function for best piece length for given size. \"\"\" value = utils . get_piece_length ( size ) assert value % 1024 == 0 test_get_piece_length_max ( size ) Test function for best piece length for given size maximum. Source code in tests\\test_utils.py 39 40 41 42 43 44 45 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_max ( size ): \"\"\" Test function for best piece length for given size maximum. \"\"\" value = utils . get_piece_length ( size ) assert value < 2 ** 27 test_get_piece_length_min ( size ) Test function for best piece length for given size minimum. Source code in tests\\test_utils.py 48 49 50 51 52 53 54 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_min ( size ): \"\"\" Test function for best piece length for given size minimum. \"\"\" value = utils . get_piece_length ( size ) assert value >= 2 ** 14 test_humanize_bytes ( amount , result ) Test humanize bytes function. Source code in tests\\test_utils.py 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 @pytest . mark . parametrize ( \"amount, result\" , [ ( 100 , \"100\" ), ( 1100 , \"1 KiB\" ), ( 1_100_000 , \"1 MiB\" ), ( 1_100_000_000 , \"1 GiB\" ), ( 4_400_120_000 , \"4 GiB\" ), ( 4_000_120_000 , \"3 GiB\" ), ], ) def test_humanize_bytes ( amount , result ): \"\"\" Test humanize bytes function. \"\"\" assert utils . humanize_bytes ( amount ) == result test_missing_path_error () Test exception for missing path parameter. Source code in tests\\test_utils.py 137 138 139 140 141 142 143 144 145 def test_missing_path_error (): \"\"\" Test exception for missing path parameter. \"\"\" try : raise utils . MissingPathError ( \"message\" ) except utils . MissingPathError : assert True assert dir2 test_next_power_2 ( value ) Test next power of 2 function in utils module. Source code in tests\\test_utils.py 148 149 150 151 152 153 154 155 156 157 @pytest . mark . parametrize ( \"value\" , [ 5 , 32 , 18 , 225 , 16384 , 256000 ]) def test_next_power_2 ( value ): \"\"\" Test next power of 2 function in utils module. \"\"\" result = utils . next_power_2 ( value ) log = math . log2 ( result ) assert log == int ( log ) assert result % 2 == 0 assert result >= value test_norm_plength_errors ( amount ) Test function to normalize piece length errors. Parameters: Name Type Description Default amount any arguments intended to raise an exception. required Source code in tests\\test_utils.py 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"amount\" , [ \"hello\" , 11 , 0 , 100000 , 28 , \"zero\" , \"fifteen\" ] ) def test_norm_plength_errors ( amount ): \"\"\"Test function to normalize piece length errors. Parameters ---------- amount : any arguments intended to raise an exception. \"\"\" try : assert utils . normalize_piece_length ( amount ) except utils . PieceLengthValueError : assert True test_normalize_piece_length_int ( amount , result ) Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 178 179 180 181 182 183 184 185 186 187 188 189 @pytest . mark . parametrize ( \"amount, result\" , [( i , 2 ** i ) for i in range ( 14 , 25 )]) def test_normalize_piece_length_int ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result test_normalize_piece_length_str ( amount , result ) Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 192 193 194 195 196 197 198 199 200 201 202 203 204 205 @pytest . mark . parametrize ( \"amount, result\" , [( str ( i ), 2 ** i ) for i in range ( 14 , 21 )] ) def test_normalize_piece_length_str ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result test_path_stat ( dir1 ) Test function for acquiring piece length information on folder. Source code in tests\\test_utils.py 78 79 80 81 82 83 def test_path_stat ( dir1 ): \"\"\" Test function for acquiring piece length information on folder. \"\"\" _ , _ , piece_length = utils . path_stat ( dir1 ) assert piece_length % ( 2 ** 14 ) == 0 test_path_stat_filelist_size ( dir1 ) Test function for acquiring file list information on folder. Source code in tests\\test_utils.py 94 95 96 97 98 99 def test_path_stat_filelist_size ( dir1 ): \"\"\" Test function for acquiring file list information on folder. \"\"\" filelist , _ , _ = utils . path_stat ( dir1 ) assert len ( filelist ) == 8 test_path_stat_size ( dir1 ) Test function for acquiring total size information on folder. Source code in tests\\test_utils.py 86 87 88 89 90 91 def test_path_stat_size ( dir1 ): \"\"\" Test function for acquiring total size information on folder. \"\"\" _ , totalsize , _ = utils . path_stat ( dir1 ) assert totalsize == ( 2 ** 18 ) * 8 test_piecelength_error_fixtures () Test exception for uninterpretable piece length value. Source code in tests\\test_utils.py 126 127 128 129 130 131 132 133 134 def test_piecelength_error_fixtures (): \"\"\" Test exception for uninterpretable piece length value. \"\"\" try : raise utils . PieceLengthValueError ( \"message\" ) except utils . PieceLengthValueError : assert True assert dir1","title":"Source Code"},{"location":"source/#torrentfile_1","text":"","title":"TorrentFile"},{"location":"source/#api-and-source","text":"","title":"API and Source"},{"location":"source/#torrent-module","text":"module torrentfile. torrent Classes and procedures pertaining to the creation of torrent meta files.","title":"Torrent Module"},{"location":"source/#classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. Classes MetaFile \u2014 Base Class for all TorrentFile classes. TorrentFile \u2014 Class for creating Bittorrent meta files. TorrentFileV2 \u2014 Class for creating Bittorrent meta v2 files. TorrentFileHybrid \u2014 Construct the Hybrid torrent meta file with provided parameters. TorrentAssembler \u2014 Assembler class for Bittorrent version 2 and hybrid meta files.","title":"v1 meta-dictionary"},{"location":"source/#-","text":"","title":"------"},{"location":"source/#utils-module","text":"module torrentfile. utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Classes Memo \u2014 Memoice chache object. MissingPathError \u2014 Path parameter is required to specify target content. PieceLengthValueError \u2014 Piece Length parameter must equal a perfect power of 2. Functions get_file_list ( path ) (list) \u2014 Return a sorted list of file paths contained in directory. get_piece_length ( size ) (int) \u2014 Calculate the ideal piece length for bittorrent data. humanize_bytes ( amount ) (str) \u2014 Convert integer into human readable memory sized denomination. next_power_2 ( value ) (int) \u2014 Calculate the next perfect power of 2 equal to or greater than value. normalize_piece_length ( piece_length ) (int) \u2014 Verify input piece_length is valid and convert accordingly. path_piece_length ( path ) (int) \u2014 Calculate piece length for input path and contents. path_size ( path ) (int) \u2014 Return the total size of all files in path recursively. path_stat ( path ) (list) \u2014 Calculate directory statistics.","title":"Utils Module"},{"location":"source/#-_1","text":"","title":"------"},{"location":"source/#edit-module","text":"module torrentfile. edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"Edit Module"},{"location":"source/#keywords","text":"private comment source trackers web-seeds Functions edit_torrent ( metafile , args ) (dict) \u2014 Edit the properties and values in a torrent meta file. filter_empty ( args , meta , info ) \u2014 Remove dictionary keys with empty values.","title":"Keywords"},{"location":"source/#-_2","text":"","title":"------"},{"location":"source/#interactive-module","text":"module torrentfile. interactive Module contains the procedures used for Interactive Mode. Classes InteractiveEditor \u2014 Interactive dialog class for torrent editing. InteractiveCreator \u2014 Class namespace for interactive program options. Functions create_torrent ( ) \u2014 Create new torrent file interactively. edit_action ( ) \u2014 Edit the editable values of the torrent meta file. get_input ( *args ) (str) \u2014 Determine appropriate input function to call. recheck_torrent ( ) \u2014 Check torrent download completed percentage. select_action ( ) \u2014 Operate TorrentFile program interactively through terminal. showcenter ( txt ) \u2014 Print text to screen in the center position of the terminal. showtext ( txt ) \u2014 Print contents of txt to screen.","title":"Interactive Module"},{"location":"source/#-_3","text":"","title":"------"},{"location":"source/#cli-module","text":"module torrentfile. cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. Classes TorrentFileHelpFormatter \u2014 Formatting class for help tips provided by the CLI. Functions activate_logger ( ) \u2014 Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. main ( ) \u2014 Initiate main function for CLI script.","title":"CLI Module"},{"location":"source/#-_4","text":"","title":"------"},{"location":"source/#recheck-module","text":"module torrentfile. recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Classes Checker \u2014 Check a given file or directory to see if it matches a torrentfile. FeedChecker \u2014 Validates torrent content. HashChecker \u2014 Verify that root hashes of content files match the .torrent files.","title":"Recheck Module"},{"location":"source/#-_5","text":"","title":"------"},{"location":"source/#hasher-module","text":"module torrentfile. hasher Piece/File Hashers for Bittorrent meta file contents. Classes Hasher \u2014 Piece hasher for Bittorrent V1 files. HasherV2 \u2014 Calculate the root hash and piece layers for file contents. HasherHybrid \u2014 Calculate root and piece hashes for creating hybrid torrent file. FileHasher \u2014 Calculate root and piece hashes for creating hybrid torrent file. Functions merkle_root ( blocks ) (bytes) \u2014 Calculate the merkle root for a seq of sha256 hash digests.","title":"Hasher Module"},{"location":"source/#-_6","text":"","title":"------"},{"location":"source/#torrentfile","text":"Torrentfile can create Bittorrent metafiles for any content. Both Bittorrent v1 and v2 are fully supported. Also included is a torrent torrent file checker, which can verify a .torrent file is formated correctly as well as validate files and folders against metadata. Modules: metafile: Creation/Validation of v1 .torrent files. metafile2: Creation/Validation of v2 .torrent files. torrentfile: torrentfiles Command Line Interface implementation. exceptions: Custom Exceptions used in package. utils: Utilities used throughout package.","title":"torrentfile"},{"location":"source/#torrentfile.create","text":"Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args","title":"create()"},{"location":"source/#torrentfile.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args","title":"execute()"},{"location":"source/#torrentfile.info","text":"Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output","title":"info()"},{"location":"source/#torrentfile.magnet","text":"Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile : str ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri","title":"magnet()"},{"location":"source/#torrentfile.__main__","text":"Enable calling the package directly with python from the command line.","title":"__main__"},{"location":"source/#torrentfile.__main__.main","text":"Start the entry point script. Source code in torrentfile\\__main__.py 26 27 28 29 30 def main (): \"\"\" Start the entry point script. \"\"\" execute ()","title":"main()"},{"location":"source/#torrentfile.cli","text":"Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility.","title":"cli"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter","text":"Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"TorrentFileHelpFormatter"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.__init__","text":"Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions )","title":"__init__()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._format_text","text":"Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \"","title":"_format_text()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._join_parts","text":"Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts )","title":"_join_parts()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._split_lines","text":"Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ]","title":"_split_lines()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.format_headers","text":"Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"format_headers()"},{"location":"source/#torrentfile.cli.activate_logger","text":"Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" )","title":"activate_logger()"},{"location":"source/#torrentfile.cli.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args","title":"execute()"},{"location":"source/#torrentfile.cli.main","text":"Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute ()","title":"main()"},{"location":"source/#torrentfile.commands","text":"The commands module contains the Action Commands executed by the CLI script. Each function pertains to a command line action/subcommand and drives specific features of the application.","title":"commands"},{"location":"source/#torrentfile.commands--functions","text":"create_command info_command edit_command recheck_command magnet_command","title":"Functions"},{"location":"source/#torrentfile.commands.create","text":"Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args","title":"create()"},{"location":"source/#torrentfile.commands.edit","text":"Execute the edit CLI sub-command with provided arguments. Parameters: Name Type Description Default args Namespace positional and optional CLI arguments. required Returns: Type Description str path to edited torrent file. Source code in torrentfile\\commands.py 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 def edit ( args : list ): \"\"\" Execute the edit CLI sub-command with provided arguments. Parameters ---------- args : Namespace positional and optional CLI arguments. Returns ------- str path to edited torrent file. \"\"\" metafile = args . metafile logger . info ( \"Editing %s Meta File\" , str ( args . metafile )) editargs = { \"url-list\" : args . url_list , \"httpseeds\" : args . httpseeds , \"announce\" : args . announce , \"source\" : args . source , \"private\" : args . private , \"comment\" : args . comment , } return edit_torrent ( metafile , editargs )","title":"edit()"},{"location":"source/#torrentfile.commands.info","text":"Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output","title":"info()"},{"location":"source/#torrentfile.commands.magnet","text":"Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile : str ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri","title":"magnet()"},{"location":"source/#torrentfile.commands.recheck","text":"Execute recheck CLI sub-command. Parameters: Name Type Description Default args Namespace positional and optional arguments. required Returns: Type Description str The percentage of content currently saved to disk. Source code in torrentfile\\commands.py 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 def recheck ( args : list ): \"\"\" Execute recheck CLI sub-command. Parameters ---------- args : Namespace positional and optional arguments. Returns ------- str The percentage of content currently saved to disk. \"\"\" metafile = args . metafile content = args . content logger . debug ( \"Validating %s against %s contents\" , metafile , content ) checker = Checker ( metafile , content ) logger . debug ( \"Completed initialization of the Checker class\" ) result = checker . results () sys . stdout . write ( str ( result ) + \"% Match \\n \" ) sys . stdout . flush () return result","title":"recheck()"},{"location":"source/#torrentfile.edit","text":"Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"edit"},{"location":"source/#torrentfile.edit--keywords","text":"private comment source trackers web-seeds","title":"Keywords"},{"location":"source/#torrentfile.edit.edit_torrent","text":"Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta","title":"edit_torrent()"},{"location":"source/#torrentfile.edit.filter_empty","text":"Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val )","title":"filter_empty()"},{"location":"source/#torrentfile.hasher","text":"Piece/File Hashers for Bittorrent meta file contents.","title":"hasher"},{"location":"source/#torrentfile.hasher.FileHasher","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"FileHasher"},{"location":"source/#torrentfile.hasher.FileHasher.__init__","text":"Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" )","title":"__init__()"},{"location":"source/#torrentfile.hasher.FileHasher.__iter__","text":"Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.FileHasher.__next__","text":"Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash","title":"__next__()"},{"location":"source/#torrentfile.hasher.FileHasher._calculate_root","text":"Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.FileHasher._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.Hasher","text":"Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"Hasher"},{"location":"source/#torrentfile.hasher.Hasher.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ]))","title":"__init__()"},{"location":"source/#torrentfile.hasher.Hasher.__iter__","text":"Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.Hasher.__next__","text":"Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"__next__()"},{"location":"source/#torrentfile.hasher.Hasher._handle_partial","text":"Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec","title":"_handle_partial()"},{"location":"source/#torrentfile.hasher.Hasher.next_file","text":"Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False","title":"next_file()"},{"location":"source/#torrentfile.hasher.HasherHybrid","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"HasherHybrid"},{"location":"source/#torrentfile.hasher.HasherHybrid.__init__","text":"Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherHybrid._calculate_root","text":"Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherHybrid._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.HasherHybrid.process_file","text":"Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.HasherV2","text":"Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"HasherV2"},{"location":"source/#torrentfile.hasher.HasherV2.__init__","text":"Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherV2._calculate_root","text":"Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherV2.process_file","text":"Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.merkle_root","text":"Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks","title":"merkle_root()"},{"location":"source/#torrentfile.interactive","text":"Module contains the procedures used for Interactive Mode.","title":"interactive"},{"location":"source/#torrentfile.interactive.InteractiveCreator","text":"Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"InteractiveCreator"},{"location":"source/#torrentfile.interactive.InteractiveCreator.__init__","text":"Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props ()","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveCreator.get_props","text":"Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"get_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor","text":"Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"InteractiveEditor"},{"location":"source/#torrentfile.interactive.InteractiveEditor.__init__","text":"Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), }","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.edit_props","text":"Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"edit_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.sanatize_response","text":"Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val","title":"sanatize_response()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.show_current","text":"Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out )","title":"show_current()"},{"location":"source/#torrentfile.interactive._get_input","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value","title":"_get_input()"},{"location":"source/#torrentfile.interactive._get_input_loop","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" )","title":"_get_input_loop()"},{"location":"source/#torrentfile.interactive.create_torrent","text":"Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator","title":"create_torrent()"},{"location":"source/#torrentfile.interactive.edit_action","text":"Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props ()","title":"edit_action()"},{"location":"source/#torrentfile.interactive.get_input","text":"Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- *args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args )","title":"get_input()"},{"location":"source/#torrentfile.interactive.recheck_torrent","text":"Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results","title":"recheck_torrent()"},{"location":"source/#torrentfile.interactive.select_action","text":"Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover","title":"select_action()"},{"location":"source/#torrentfile.interactive.showcenter","text":"Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string )","title":"showcenter()"},{"location":"source/#torrentfile.interactive.showtext","text":"Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt )","title":"showtext()"},{"location":"source/#torrentfile.mixins","text":"Collection of classes that can be used as Mixins with other base classes. Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the progress bar mixin. And any class is eligible to use the callback mixin.","title":"mixins"},{"location":"source/#torrentfile.mixins.CbMixin","text":"Mixin class to set a callback during hashing procedure. Source code in torrentfile\\mixins.py 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class CbMixin : \"\"\" Mixin class to set a callback during hashing procedure. \"\"\" _cb = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover","title":"CbMixin"},{"location":"source/#torrentfile.mixins.CbMixin.set_callback","text":"Assign a callback to the Hashing class. Parameters: Name Type Description Default func function the callback function required Source code in torrentfile\\mixins.py 40 41 42 43 44 45 46 47 48 49 50 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover","title":"set_callback()"},{"location":"source/#torrentfile.mixins.ProgMixin","text":"Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files.","title":"ProgMixin"},{"location":"source/#torrentfile.mixins.ProgMixin--methods","text":"prog_start prog_update prog_close Source code in torrentfile\\mixins.py 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 class ProgMixin : \"\"\" Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods ------- prog_start prog_update prog_close \"\"\" def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start ) def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush () def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False","title":"Methods"},{"location":"source/#torrentfile.mixins.ProgMixin.is_active","text":"Test to see if there is an active progress bar for object. Returns: Name Type Description bool True if there is, otherwise False. Source code in torrentfile\\mixins.py 199 200 201 202 203 204 205 206 207 208 209 210 def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False","title":"is_active()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_close","text":"Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. Source code in torrentfile\\mixins.py 187 188 189 190 191 192 193 194 195 196 197 def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog","title":"prog_close()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_start","text":"Generate a new progress bar for the given file path. Parameters: Name Type Description Default total int the total amount of units accumulating towards. required path str path to file being hashed. required length int the number of characters of the actual progress bar. 50 unit str the text representation of the value being measured. None Source code in torrentfile\\mixins.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start )","title":"prog_start()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_update","text":"Update progress bar with given amount of progress. Parameters: Name Type Description Default val int the number of bytes count the progress bar should increase. required Source code in torrentfile\\mixins.py 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush ()","title":"prog_update()"},{"location":"source/#torrentfile.mixins.ProgressBar","text":"Holds the state and details of the terminal progress bars. Parameters: Name Type Description Default total int the total amount to be accumulated. required title str the subject of the progress tracker required length int the width of the progress bar required unit str the text representation incremented required Source code in torrentfile\\mixins.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 class ProgressBar : \"\"\" Holds the state and details of the terminal progress bars. Parameters ---------- total : int the total amount to be accumulated. title : str the subject of the progress tracker length : int the width of the progress bar unit : str the text representation incremented \"\"\" def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ]) def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar )","title":"ProgressBar"},{"location":"source/#torrentfile.mixins.ProgressBar.__init__","text":"Construct the progress bar object and store state of it's properties. Source code in torrentfile\\mixins.py 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ])","title":"__init__()"},{"location":"source/#torrentfile.mixins.ProgressBar.increment","text":"Increase the state of the progress bar value. Parameters: Name Type Description Default value int the amount to increment the state by. required Source code in torrentfile\\mixins.py 96 97 98 99 100 101 102 103 104 105 def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value","title":"increment()"},{"location":"source/#torrentfile.mixins.ProgressBar.pbar","text":"Return the size of the filled portion of the progress bar. Returns: Name Type Description str the progress bar characters Source code in torrentfile\\mixins.py 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar )","title":"pbar()"},{"location":"source/#torrentfile.mixins.waiting","text":"Show loading message while thread completes processing. Parameters: Name Type Description Default msg str Message string printed before the progress bar required flag list Once flag is filled exit loop required timeout int max amount of time to run the function. 180 Source code in torrentfile\\mixins.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 def waiting ( msg , flag , timeout = 180 ): \"\"\" Show loading message while thread completes processing. Parameters ---------- msg : str Message string printed before the progress bar flag : list Once flag is filled exit loop timeout : int max amount of time to run the function. \"\"\" then = time . time () codes , fill = list ( range ( 9617 , 9620 )), chr ( 9619 ) size = idx = 0 total = shutil . get_terminal_size () . columns - len ( msg ) - 20 def output ( text ): \"\"\" Print parameter message to the console. Parameters ---------- text : str output message \"\"\" sys . stdout . write ( text ) sys . stdout . flush () output ( \" \\n \" ) while not flag : time . sleep ( 0.16 ) filled = ( fill * size ) + chr ( codes [ idx ]) + ( \" \" * ( total - size )) output ( f \" { msg } : { filled } \\r \" ) idx = idx + 1 if idx + 1 < len ( codes ) else 0 size = size + 1 if size < total else 0 if time . time () - then > timeout : break output ( \" \\n \" )","title":"waiting()"},{"location":"source/#torrentfile.recheck","text":"Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole.","title":"recheck"},{"location":"source/#torrentfile.recheck.Checker","text":"Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required","title":"Checker"},{"location":"source/#torrentfile.recheck.Checker--example","text":">> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"Example"},{"location":"source/#torrentfile.recheck.Checker.__init__","text":"Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.Checker.check_paths","text":"Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], [])","title":"check_paths()"},{"location":"source/#torrentfile.recheck.Checker.find_root","text":"Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root )","title":"find_root()"},{"location":"source/#torrentfile.recheck.Checker.iter_hashes","text":"Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"iter_hashes()"},{"location":"source/#torrentfile.recheck.Checker.log_msg","text":"Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message )","title":"log_msg()"},{"location":"source/#torrentfile.recheck.Checker.piece_checker","text":"Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker","title":"piece_checker()"},{"location":"source/#torrentfile.recheck.Checker.register_callback","text":"Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook","title":"register_callback()"},{"location":"source/#torrentfile.recheck.Checker.results","text":"Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result","title":"results()"},{"location":"source/#torrentfile.recheck.Checker.walk_file_tree","text":"Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ])","title":"walk_file_tree()"},{"location":"source/#torrentfile.recheck.FeedChecker","text":"Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"FeedChecker"},{"location":"source/#torrentfile.recheck.FeedChecker.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 335 336 337 338 339 340 341 342 343 344 345 346 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None","title":"__init__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 348 349 350 351 352 353 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__next__","text":"Yield back result of comparison. Source code in torrentfile\\recheck.py 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial )","title":"__next__()"},{"location":"source/#torrentfile.recheck.FeedChecker._gen_padding","text":"Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"_gen_padding()"},{"location":"source/#torrentfile.recheck.FeedChecker.extract","text":"Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad","title":"extract()"},{"location":"source/#torrentfile.recheck.FeedChecker.iter_pieces","text":"Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close ()","title":"iter_pieces()"},{"location":"source/#torrentfile.recheck.HashChecker","text":"Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required Source code in torrentfile\\recheck.py 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"HashChecker"},{"location":"source/#torrentfile.recheck.HashChecker.Padder","text":"Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"Padder"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__init__","text":"Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 530 531 532 533 534 535 536 537 538 539 540 541 542 543 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__iter__","text":"Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 545 546 547 548 549 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__next__","text":"Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.__init__","text":"Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 487 488 489 490 491 492 493 494 495 496 497 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 499 500 501 502 503 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.__next__","text":"Provide the result of comparison. Source code in torrentfile\\recheck.py 505 506 507 508 509 510 511 512 513 514 515 516 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.advance","text":"Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"advance()"},{"location":"source/#torrentfile.recheck.HashChecker.next_file","text":"Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False","title":"next_file()"},{"location":"source/#torrentfile.recheck.HashChecker.process_current","text":"Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err","title":"process_current()"},{"location":"source/#torrentfile.torrent","text":"Classes and procedures pertaining to the creation of torrent meta files.","title":"torrent"},{"location":"source/#torrentfile.torrent--classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#torrentfile.torrent--constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#torrentfile.torrent--bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#torrentfile.torrent--meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#torrentfile.torrent--bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#torrentfile.torrent--v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory.","title":"v1 meta-dictionary"},{"location":"source/#torrentfile.torrent.MetaFile","text":"Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"MetaFile"},{"location":"source/#torrentfile.torrent.MetaFile.__init__","text":"Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name","title":"__init__()"},{"location":"source/#torrentfile.torrent.MetaFile.assemble","text":"Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError","title":"assemble()"},{"location":"source/#torrentfile.torrent.MetaFile.set_callback","text":"Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func )","title":"set_callback()"},{"location":"source/#torrentfile.torrent.MetaFile.sort_meta","text":"Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta","title":"sort_meta()"},{"location":"source/#torrentfile.torrent.MetaFile.write","text":"Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"write()"},{"location":"source/#torrentfile.torrent.TorrentAssembler","text":"Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentAssembler"},{"location":"source/#torrentfile.torrent.TorrentAssembler.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentAssembler._traverse","text":"Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentAssembler.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFile","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- **kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"TorrentFile"},{"location":"source/#torrentfile.torrent.TorrentFile.__init__","text":"Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFile.assemble","text":"Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid","text":"Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentFileHybrid"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid._traverse","text":"Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileV2","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"TorrentFileV2"},{"location":"source/#torrentfile.torrent.TorrentFileV2.__init__","text":"Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileV2._traverse","text":"Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileV2.assemble","text":"Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers","title":"assemble()"},{"location":"source/#torrentfile.utils","text":"Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field.","title":"utils"},{"location":"source/#torrentfile.utils.Memo","text":"Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"Memo"},{"location":"source/#torrentfile.utils.Memo.__call__","text":"Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"__call__()"},{"location":"source/#torrentfile.utils.Memo.__init__","text":"Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {}","title":"__init__()"},{"location":"source/#torrentfile.utils.MissingPathError","text":"Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"MissingPathError"},{"location":"source/#torrentfile.utils.MissingPathError.__init__","text":"Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils.PieceLengthValueError","text":"Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"PieceLengthValueError"},{"location":"source/#torrentfile.utils.PieceLengthValueError.__init__","text":"Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils._filelist_total","text":"Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist )","title":"_filelist_total()"},{"location":"source/#torrentfile.utils.filelist_total","text":"Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError","title":"filelist_total()"},{"location":"source/#torrentfile.utils.get_file_list","text":"Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist","title":"get_file_list()"},{"location":"source/#torrentfile.utils.get_piece_length","text":"Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp","title":"get_piece_length()"},{"location":"source/#torrentfile.utils.humanize_bytes","text":"Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\"","title":"humanize_bytes()"},{"location":"source/#torrentfile.utils.next_power_2","text":"Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start","title":"next_power_2()"},{"location":"source/#torrentfile.utils.normalize_piece_length","text":"Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError","title":"normalize_piece_length()"},{"location":"source/#torrentfile.utils.path_piece_length","text":"Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize )","title":"path_piece_length()"},{"location":"source/#torrentfile.utils.path_size","text":"Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size","title":"path_size()"},{"location":"source/#torrentfile.utils.path_stat","text":"Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length )","title":"path_stat()"},{"location":"source/#torrentfile.version","text":"Holds the release version number.","title":"version"},{"location":"source/#torrentfile.cli","text":"Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility.","title":"cli"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter","text":"Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"TorrentFileHelpFormatter"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.__init__","text":"Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions )","title":"__init__()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._format_text","text":"Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \"","title":"_format_text()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._join_parts","text":"Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts )","title":"_join_parts()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._split_lines","text":"Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ]","title":"_split_lines()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.format_headers","text":"Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"format_headers()"},{"location":"source/#torrentfile.cli.activate_logger","text":"Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" )","title":"activate_logger()"},{"location":"source/#torrentfile.cli.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args","title":"execute()"},{"location":"source/#torrentfile.cli.main","text":"Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute ()","title":"main()"},{"location":"source/#torrentfile.edit","text":"Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"edit"},{"location":"source/#torrentfile.edit--keywords","text":"private comment source trackers web-seeds","title":"Keywords"},{"location":"source/#torrentfile.edit.edit_torrent","text":"Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta","title":"edit_torrent()"},{"location":"source/#torrentfile.edit.filter_empty","text":"Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val )","title":"filter_empty()"},{"location":"source/#torrentfile.hasher","text":"Piece/File Hashers for Bittorrent meta file contents.","title":"hasher"},{"location":"source/#torrentfile.hasher.FileHasher","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"FileHasher"},{"location":"source/#torrentfile.hasher.FileHasher.__init__","text":"Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" )","title":"__init__()"},{"location":"source/#torrentfile.hasher.FileHasher.__iter__","text":"Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.FileHasher.__next__","text":"Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash","title":"__next__()"},{"location":"source/#torrentfile.hasher.FileHasher._calculate_root","text":"Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.FileHasher._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.Hasher","text":"Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"Hasher"},{"location":"source/#torrentfile.hasher.Hasher.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ]))","title":"__init__()"},{"location":"source/#torrentfile.hasher.Hasher.__iter__","text":"Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.Hasher.__next__","text":"Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"__next__()"},{"location":"source/#torrentfile.hasher.Hasher._handle_partial","text":"Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec","title":"_handle_partial()"},{"location":"source/#torrentfile.hasher.Hasher.next_file","text":"Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False","title":"next_file()"},{"location":"source/#torrentfile.hasher.HasherHybrid","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"HasherHybrid"},{"location":"source/#torrentfile.hasher.HasherHybrid.__init__","text":"Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherHybrid._calculate_root","text":"Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherHybrid._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.HasherHybrid.process_file","text":"Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.HasherV2","text":"Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"HasherV2"},{"location":"source/#torrentfile.hasher.HasherV2.__init__","text":"Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherV2._calculate_root","text":"Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherV2.process_file","text":"Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.merkle_root","text":"Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks","title":"merkle_root()"},{"location":"source/#torrentfile.interactive","text":"Module contains the procedures used for Interactive Mode.","title":"interactive"},{"location":"source/#torrentfile.interactive.InteractiveCreator","text":"Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"InteractiveCreator"},{"location":"source/#torrentfile.interactive.InteractiveCreator.__init__","text":"Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props ()","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveCreator.get_props","text":"Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"get_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor","text":"Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"InteractiveEditor"},{"location":"source/#torrentfile.interactive.InteractiveEditor.__init__","text":"Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), }","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.edit_props","text":"Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"edit_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.sanatize_response","text":"Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val","title":"sanatize_response()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.show_current","text":"Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out )","title":"show_current()"},{"location":"source/#torrentfile.interactive._get_input","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value","title":"_get_input()"},{"location":"source/#torrentfile.interactive._get_input_loop","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" )","title":"_get_input_loop()"},{"location":"source/#torrentfile.interactive.create_torrent","text":"Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator","title":"create_torrent()"},{"location":"source/#torrentfile.interactive.edit_action","text":"Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props ()","title":"edit_action()"},{"location":"source/#torrentfile.interactive.get_input","text":"Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- *args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args )","title":"get_input()"},{"location":"source/#torrentfile.interactive.recheck_torrent","text":"Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results","title":"recheck_torrent()"},{"location":"source/#torrentfile.interactive.select_action","text":"Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover","title":"select_action()"},{"location":"source/#torrentfile.interactive.showcenter","text":"Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string )","title":"showcenter()"},{"location":"source/#torrentfile.interactive.showtext","text":"Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt )","title":"showtext()"},{"location":"source/#torrentfile.recheck","text":"Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole.","title":"recheck"},{"location":"source/#torrentfile.recheck.Checker","text":"Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required","title":"Checker"},{"location":"source/#torrentfile.recheck.Checker--example","text":">> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"Example"},{"location":"source/#torrentfile.recheck.Checker.__init__","text":"Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.Checker.check_paths","text":"Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], [])","title":"check_paths()"},{"location":"source/#torrentfile.recheck.Checker.find_root","text":"Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root )","title":"find_root()"},{"location":"source/#torrentfile.recheck.Checker.iter_hashes","text":"Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"iter_hashes()"},{"location":"source/#torrentfile.recheck.Checker.log_msg","text":"Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message )","title":"log_msg()"},{"location":"source/#torrentfile.recheck.Checker.piece_checker","text":"Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker","title":"piece_checker()"},{"location":"source/#torrentfile.recheck.Checker.register_callback","text":"Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook","title":"register_callback()"},{"location":"source/#torrentfile.recheck.Checker.results","text":"Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result","title":"results()"},{"location":"source/#torrentfile.recheck.Checker.walk_file_tree","text":"Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ])","title":"walk_file_tree()"},{"location":"source/#torrentfile.recheck.FeedChecker","text":"Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"FeedChecker"},{"location":"source/#torrentfile.recheck.FeedChecker.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 335 336 337 338 339 340 341 342 343 344 345 346 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None","title":"__init__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 348 349 350 351 352 353 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__next__","text":"Yield back result of comparison. Source code in torrentfile\\recheck.py 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial )","title":"__next__()"},{"location":"source/#torrentfile.recheck.FeedChecker._gen_padding","text":"Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"_gen_padding()"},{"location":"source/#torrentfile.recheck.FeedChecker.extract","text":"Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad","title":"extract()"},{"location":"source/#torrentfile.recheck.FeedChecker.iter_pieces","text":"Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close ()","title":"iter_pieces()"},{"location":"source/#torrentfile.recheck.HashChecker","text":"Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required Source code in torrentfile\\recheck.py 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"HashChecker"},{"location":"source/#torrentfile.recheck.HashChecker.Padder","text":"Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"Padder"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__init__","text":"Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 530 531 532 533 534 535 536 537 538 539 540 541 542 543 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__iter__","text":"Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 545 546 547 548 549 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__next__","text":"Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.__init__","text":"Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 487 488 489 490 491 492 493 494 495 496 497 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 499 500 501 502 503 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.__next__","text":"Provide the result of comparison. Source code in torrentfile\\recheck.py 505 506 507 508 509 510 511 512 513 514 515 516 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.advance","text":"Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"advance()"},{"location":"source/#torrentfile.recheck.HashChecker.next_file","text":"Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False","title":"next_file()"},{"location":"source/#torrentfile.recheck.HashChecker.process_current","text":"Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err","title":"process_current()"},{"location":"source/#torrentfile.torrent","text":"Classes and procedures pertaining to the creation of torrent meta files.","title":"torrent"},{"location":"source/#torrentfile.torrent--classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#torrentfile.torrent--constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#torrentfile.torrent--bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#torrentfile.torrent--meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#torrentfile.torrent--bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#torrentfile.torrent--v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory.","title":"v1 meta-dictionary"},{"location":"source/#torrentfile.torrent.MetaFile","text":"Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"MetaFile"},{"location":"source/#torrentfile.torrent.MetaFile.__init__","text":"Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name","title":"__init__()"},{"location":"source/#torrentfile.torrent.MetaFile.assemble","text":"Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError","title":"assemble()"},{"location":"source/#torrentfile.torrent.MetaFile.set_callback","text":"Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func )","title":"set_callback()"},{"location":"source/#torrentfile.torrent.MetaFile.sort_meta","text":"Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta","title":"sort_meta()"},{"location":"source/#torrentfile.torrent.MetaFile.write","text":"Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"write()"},{"location":"source/#torrentfile.torrent.TorrentAssembler","text":"Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentAssembler"},{"location":"source/#torrentfile.torrent.TorrentAssembler.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentAssembler._traverse","text":"Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentAssembler.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFile","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- **kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"TorrentFile"},{"location":"source/#torrentfile.torrent.TorrentFile.__init__","text":"Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFile.assemble","text":"Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid","text":"Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentFileHybrid"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid._traverse","text":"Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileV2","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"TorrentFileV2"},{"location":"source/#torrentfile.torrent.TorrentFileV2.__init__","text":"Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileV2._traverse","text":"Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileV2.assemble","text":"Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers","title":"assemble()"},{"location":"source/#torrentfile.utils","text":"Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field.","title":"utils"},{"location":"source/#torrentfile.utils.Memo","text":"Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"Memo"},{"location":"source/#torrentfile.utils.Memo.__call__","text":"Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"__call__()"},{"location":"source/#torrentfile.utils.Memo.__init__","text":"Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {}","title":"__init__()"},{"location":"source/#torrentfile.utils.MissingPathError","text":"Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"MissingPathError"},{"location":"source/#torrentfile.utils.MissingPathError.__init__","text":"Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils.PieceLengthValueError","text":"Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"PieceLengthValueError"},{"location":"source/#torrentfile.utils.PieceLengthValueError.__init__","text":"Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils._filelist_total","text":"Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist )","title":"_filelist_total()"},{"location":"source/#torrentfile.utils.filelist_total","text":"Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError","title":"filelist_total()"},{"location":"source/#torrentfile.utils.get_file_list","text":"Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist","title":"get_file_list()"},{"location":"source/#torrentfile.utils.get_piece_length","text":"Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp","title":"get_piece_length()"},{"location":"source/#torrentfile.utils.humanize_bytes","text":"Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\"","title":"humanize_bytes()"},{"location":"source/#torrentfile.utils.next_power_2","text":"Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start","title":"next_power_2()"},{"location":"source/#torrentfile.utils.normalize_piece_length","text":"Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError","title":"normalize_piece_length()"},{"location":"source/#torrentfile.utils.path_piece_length","text":"Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize )","title":"path_piece_length()"},{"location":"source/#torrentfile.utils.path_size","text":"Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size","title":"path_size()"},{"location":"source/#torrentfile.utils.path_stat","text":"Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length )","title":"path_stat()"},{"location":"source/#torrentfile.version","text":"Holds the release version number.","title":"version"},{"location":"source/#tests","text":"Unittest package init module.","title":"tests"},{"location":"source/#tests.dir1","text":"Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 168 169 170 171 172 173 174 175 176 177 178 179 @pytest . fixture ( scope = \"package\" ) def dir1 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir () yield root rmpath ( root )","title":"dir1()"},{"location":"source/#tests.dir2","text":"Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 182 183 184 185 186 187 188 189 190 191 192 193 @pytest . fixture () def dir2 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir ( ext = \"2\" ) yield Path ( root ) rmpath ( root )","title":"dir2()"},{"location":"source/#tests.file1","text":"Return the path to a temporary file package scope. Source code in tests\\__init__.py 241 242 243 244 245 246 247 248 @pytest . fixture ( scope = \"package\" ) def file1 (): \"\"\" Return the path to a temporary file package scope. \"\"\" path = tempfile () yield path rmpath ( path )","title":"file1()"},{"location":"source/#tests.file2","text":"Return the path to a temporary file no scope. Source code in tests\\__init__.py 297 298 299 300 301 302 303 304 @pytest . fixture () def file2 (): \"\"\" Return the path to a temporary file no scope. \"\"\" path = tempfile () yield path rmpath ( path )","title":"file2()"},{"location":"source/#tests.filemeta1","text":"Test fixture for generating metafile for all versions of torrents. Source code in tests\\__init__.py 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 @pytest . fixture ( scope = \"package\" , params = torrents ()) def filemeta1 ( file1 , request ): \"\"\" Test fixture for generating metafile for all versions of torrents. \"\"\" args = { \"path\" : file1 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file1 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile )","title":"filemeta1()"},{"location":"source/#tests.filemeta2","text":"Test fixture for generating a meta file no scope. Source code in tests\\__init__.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 @pytest . fixture ( params = torrents ()) def filemeta2 ( file2 , request ): \"\"\" Test fixture for generating a meta file no scope. \"\"\" args = { \"path\" : file2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url7\" , \"url8\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file2 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile )","title":"filemeta2()"},{"location":"source/#tests.metafile1","text":"Create a standard metafile for testing. Source code in tests\\__init__.py 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 @pytest . fixture ( scope = \"package\" , params = torrents ()) def metafile1 ( dir1 , request ): \"\"\" Create a standard metafile for testing. \"\"\" versions = torrents () args = { \"path\" : dir1 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir1 ) + str ( versions . index ( torrent_class )) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile )","title":"metafile1()"},{"location":"source/#tests.metafile2","text":"Create a standard metafile for testing. Source code in tests\\__init__.py 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 @pytest . fixture ( params = torrents ()) def metafile2 ( dir2 , request ): \"\"\" Create a standard metafile for testing. \"\"\" args = { \"path\" : dir2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"httpseeds\" : [ \"url6\" , \"url7\" ], \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir2 ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile )","title":"metafile2()"},{"location":"source/#tests.rmpath","text":"Remove file or directory path. Parameters: Name Type Description Default *args list Filesystem locations for removing. () Source code in tests\\__init__.py 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def rmpath ( * args ): \"\"\"Remove file or directory path. Parameters ---------- *args : list Filesystem locations for removing. \"\"\" for arg in args : if not os . path . exists ( arg ): continue if os . path . isdir ( arg ): try : shutil . rmtree ( arg ) except PermissionError : # pragma: nocover pass elif os . path . isfile ( arg ): try : os . remove ( arg ) except PermissionError : # pragma: nocover pass","title":"rmpath()"},{"location":"source/#tests.sizedfiles","text":"Generate variable sized meta files for testing, no scope. Source code in tests\\__init__.py 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 @pytest . fixture ( params = torrents ()) def sizedfiles ( dir2 , sizes , request ): \"\"\" Generate variable sized meta files for testing, no scope. \"\"\" versions = torrents () args = { \"content\" : dir2 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , \"piece_length\" : sizes , } torrent_class = request . param version = str ( versions . index ( torrent_class )) outfile = str ( dir2 ) + version + str ( sizes ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile )","title":"sizedfiles()"},{"location":"source/#tests.sizes","text":"Generate powers of 2 for file creation package scope. Source code in tests\\__init__.py 159 160 161 162 163 164 165 @pytest . fixture ( scope = \"package\" , params = [ 2 ** i for i in range ( 15 , 20 )]) def sizes ( request ): \"\"\" Generate powers of 2 for file creation package scope. \"\"\" size = request . param yield size","title":"sizes()"},{"location":"source/#tests.teardown","text":"Remove all temporary directories and files. Source code in tests\\__init__.py 141 142 143 144 145 146 147 148 149 @atexit . register def teardown (): # pragma: nocover \"\"\" Remove all temporary directories and files. \"\"\" root = Path ( __file__ ) . parent / \"TESTDIR\" for path in [ root , \"./torrentfile.log\" ]: if os . path . exists ( path ): rmpath ( path )","title":"teardown()"},{"location":"source/#tests.tempdir","text":"Create temporary directory. Parameters: Name Type Description Default ext str , optional extension to file names, by default \"1\" '1' Returns: Type Description str path to common root for directory. Source code in tests\\__init__.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 def tempdir ( ext = \"1\" ): \"\"\"Create temporary directory. Parameters ---------- ext : str, optional extension to file names, by default \"1\" Returns ------- str path to common root for directory. \"\"\" layouts = { \"1\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.mp4\" , f \"dir { ext } /file3.mp3\" , f \"dir { ext } /file4.zip\" , f \"dir { ext } /file5.txt\" , f \"dir { ext } /subdir1/subdir2/file.7z\" , f \"dir { ext } /subdir/subdir/file4.rar\" , f \"dir { ext } /subdir/subdir/file4.r01\" , ], \"2\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.jpg\" , f \"dir { ext } /subdir/file2.mp4\" , f \"dir { ext } /subdir/file3.mp3\" , ], } paths = [] for path in layouts [ ext ]: temps = tempfile ( path = path , exp = 18 ) paths . append ( temps ) return os . path . commonpath ( paths )","title":"tempdir()"},{"location":"source/#tests.tempfile","text":"Create temporary file. Creates a temporary file for unittesting purposes.py Parameters: Name Type Description Default path str , optional relative path to temporary files, by default None None exp int , optional Exponent used to determine size of file., by default 18 18 Returns: Type Description str absolute path to file. Source code in tests\\__init__.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 def tempfile ( path = None , exp = 18 ): \"\"\"Create temporary file. Creates a temporary file for unittesting purposes.py Parameters ---------- path : str, optional relative path to temporary files, by default None exp : int, optional Exponent used to determine size of file., by default 18 Returns ------- str absolute path to file. \"\"\" seq = ( string . printable + string . whitespace ) . encode ( \"utf-8\" ) root = Path ( __file__ ) . parent / \"TESTDIR\" if not os . path . exists ( root ): os . mkdir ( root ) if not path : path = root / ( str ( datetime . timestamp ( datetime . now ())) + \".file\" ) parts = Path ( path ) . parts partial = root for i , part in enumerate ( parts ): partial = partial / part if i == len ( parts ) - 1 : with open ( partial , \"wb\" ) as binfile : size = 2 ** exp while size > 0 : if len ( seq ) < size : binfile . write ( seq ) size -= len ( seq ) seq += seq else : binfile . write ( seq [: size ]) size -= size else : if not os . path . exists ( partial ): os . mkdir ( partial ) return partial","title":"tempfile()"},{"location":"source/#tests.torrents","text":"Return seq of torrentfile objects. Source code in tests\\__init__.py 152 153 154 155 156 def torrents (): \"\"\" Return seq of torrentfile objects. \"\"\" return [ TorrentFile , TorrentFileV2 , TorrentFileHybrid , TorrentAssembler ]","title":"torrents()"},{"location":"source/#tests.test_cli","text":"Testing functions for the command line interface.","title":"test_cli"},{"location":"source/#tests.test_cli.folder","text":"Yield a folder object as fixture. Source code in tests\\test_cli.py 41 42 43 44 45 46 47 48 49 @pytest . fixture ( scope = \"module\" ) def folder ( dir1 ): \"\"\" Yield a folder object as fixture. \"\"\" sfolder = str ( dir1 ) torrent = sfolder + \".torrent\" yield ( sfolder , torrent ) rmpath ( torrent )","title":"folder()"},{"location":"source/#tests.test_cli.test_cli_announce","text":"Test announce cli flag. Source code in tests\\test_cli.py 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce ( folder , piece_length , version ): \"\"\" Test announce cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--tracker\" , \"https://announce.org/tracker\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"announce\" ] == \"https://announce.org/tracker\"","title":"test_cli_announce()"},{"location":"source/#tests.test_cli.test_cli_announce_list","text":"Test announce-list cli flag. Source code in tests\\test_cli.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce_list ( folder , version ): \"\"\" Test announce-list cli flag. \"\"\" folder , torrent = folder trackers = [ \"https://announce.org/tracker\" , \"https://announce.net/tracker\" , \"https://tracker.net/announce\" , ] args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , version , \"--tracker\" , ] + trackers sys . argv = args execute () meta = pyben . load ( torrent ) for url in trackers : assert url in [ j for i in meta [ \"announce-list\" ] for j in i ]","title":"test_cli_announce_list()"},{"location":"source/#tests.test_cli.test_cli_announce_path","text":"Test CLI when path is placed after the trackers flag. Source code in tests\\test_cli.py 451 452 453 454 455 456 457 458 459 460 461 462 463 @pytest . mark . parametrize ( \"flag\" , [ \"-t\" , \"-w\" , \"--announce\" , \"--web-seed\" , \"--http-seed\" ] ) def test_cli_announce_path ( dir1 , flag ): \"\"\" Test CLI when path is placed after the trackers flag. \"\"\" args = [ \"torrentfile\" , \"create\" , flag , \"https://announce1.org\" , str ( dir1 )] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_announce_path()"},{"location":"source/#tests.test_cli.test_cli_comment","text":"Test comment cli flag. Source code in tests\\test_cli.py 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_comment ( folder , piece_length , version ): \"\"\" Test comment cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--magnet\" , \"--comment\" , \"this is a comment\" , \"--progress\" , \"1\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"comment\" ] == \"this is a comment\"","title":"test_cli_comment()"},{"location":"source/#tests.test_cli.test_cli_created_by","text":"Test if created torrents recieve a created by field in meta info. Source code in tests\\test_cli.py 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_created_by ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a created by field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"TorrentFile\" in meta [ \"created by\" ]","title":"test_cli_created_by()"},{"location":"source/#tests.test_cli.test_cli_creation_date","text":"Test if torrents created get an accurate timestamp. Source code in tests\\test_cli.py 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_creation_date ( folder , piece_length , version ): \"\"\" Test if torrents created get an accurate timestamp. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) num = float ( meta [ \"creation date\" ]) date = datetime . datetime . fromtimestamp ( num ) now = datetime . datetime . now () assert date . day == now . day assert date . year == now . year assert date . month == now . month","title":"test_cli_creation_date()"},{"location":"source/#tests.test_cli.test_cli_cwd","text":"Test outfile cli flag. Source code in tests\\test_cli.py 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 def test_cli_cwd ( folder ): \"\"\" Test outfile cli flag. \"\"\" folder , _ = folder args = [ \"torrentfile\" , \"create\" , \"--cwd\" , folder , ] sys . argv = args current = os . getcwd () name = os . path . basename ( folder ) outfile = os . path . join ( current , name ) + \".torrent\" execute () assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_cwd()"},{"location":"source/#tests.test_cli.test_cli_empty_files","text":"Test creating torrent with empty files. Source code in tests\\test_cli.py 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"progress\" , [ \"0\" , \"1\" ]) def test_cli_empty_files ( dir2 , version , progress ): \"\"\" Test creating torrent with empty files. \"\"\" args = [ \"torrentfile\" , \"create\" , str ( dir2 ), \"--meta-version\" , version , \"--source\" , \"somesource\" , \"--prog\" , progress , ] sys . argv = args def walk ( root , count ): \"\"\" Traverse directory to edit files. \"\"\" if root . is_file (): with open ( root , \"wb\" ) as _ : return 1 elif root . is_dir (): for item in root . iterdir (): if count >= 2 : break count += walk ( item , count ) return count walk ( dir2 , 0 ) execute () outfile = str ( dir2 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_empty_files()"},{"location":"source/#tests.test_cli.test_cli_help","text":"Test showing help notice cli flag. Source code in tests\\test_cli.py 352 353 354 355 356 357 358 359 360 361 def test_cli_help (): \"\"\" Test showing help notice cli flag. \"\"\" args = [ \"-h\" ] sys . argv = args try : assert execute () except SystemExit : assert True","title":"test_cli_help()"},{"location":"source/#tests.test_cli.test_cli_outfile","text":"Test outfile cli flag. Source code in tests\\test_cli.py 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_outfile ( dir1 , piece_length , version ): \"\"\" Test outfile cli flag. \"\"\" outfile = dir1 + \"test.torrent\" args = [ \"torrentfile\" , \"create\" , dir1 , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-o\" , outfile , \"--prog\" , \"1\" , ] sys . argv = args execute () assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_outfile()"},{"location":"source/#tests.test_cli.test_cli_piece_length","text":"Test piece length cli flag. Source code in tests\\test_cli.py 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_piece_length ( folder , piece_length , version ): \"\"\" Test piece length cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--progress\" , \"0\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"piece length\" ] == piece_length","title":"test_cli_piece_length()"},{"location":"source/#tests.test_cli.test_cli_private","text":"Test private cli flag. Source code in tests\\test_cli.py 85 86 87 88 89 90 91 92 93 94 def test_cli_private ( folder ): \"\"\" Test private cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--private\" ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"private\" in meta [ \"info\" ]","title":"test_cli_private()"},{"location":"source/#tests.test_cli.test_cli_slash_outpath","text":"Test if output when outpath ends with a /. Source code in tests\\test_cli.py 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 @pytest . mark . parametrize ( \"sep\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_outpath ( dir1 , sep ): \"\"\" Test if output when outpath ends with a /. \"\"\" if sys . platform != \"win32\" : sep = \"/\" # pragma: nocover parent = os . path . dirname ( dir1 ) + sep args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , \"-o\" , parent , str ( dir1 ), ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_slash_outpath()"},{"location":"source/#tests.test_cli.test_cli_slash_path","text":"Test if output when path ends with a /. Source code in tests\\test_cli.py 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 @pytest . mark . parametrize ( \"ending\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_path ( dir1 , ending ): \"\"\" Test if output when path ends with a /. \"\"\" if sys . platform != \"win32\" and ending == \" \\\\ \" : # pragma: nocover ending = \"/\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , str ( dir1 ) + ending , ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_slash_path()"},{"location":"source/#tests.test_cli.test_cli_v1","text":"Basic create torrent cli command. Source code in tests\\test_cli.py 52 53 54 55 56 57 58 59 60 def test_cli_v1 ( folder ): \"\"\" Basic create torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_v1()"},{"location":"source/#tests.test_cli.test_cli_v2","text":"Create torrent v2 cli command. Source code in tests\\test_cli.py 63 64 65 66 67 68 69 70 71 def test_cli_v2 ( folder ): \"\"\" Create torrent v2 cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"2\" ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_v2()"},{"location":"source/#tests.test_cli.test_cli_v3","text":"Create hybrid torrent cli command. Source code in tests\\test_cli.py 74 75 76 77 78 79 80 81 82 def test_cli_v3 ( folder ): \"\"\" Create hybrid torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"3\" ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_v3()"},{"location":"source/#tests.test_cli.test_cli_web_seeds","text":"Test if created torrents recieve a web seeds field in meta info. Source code in tests\\test_cli.py 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_web_seeds ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a web seeds field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-w\" , \"https://webseed.url/1\" , \"https://webseed.url/2\" , \"https://webseed.url/3\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"https://webseed.url/1\" in meta [ \"url-list\" ]","title":"test_cli_web_seeds()"},{"location":"source/#tests.test_cli.test_cli_with_debug","text":"Test debug mode cli flag. Source code in tests\\test_cli.py 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_debug ( folder , piece_length , version ): \"\"\" Test debug mode cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_with_debug()"},{"location":"source/#tests.test_cli.test_cli_with_source","text":"Test source cli flag. Source code in tests\\test_cli.py 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_source ( folder , piece_length , version ): \"\"\" Test source cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--source\" , \"somesource\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"source\" ] == \"somesource\"","title":"test_cli_with_source()"},{"location":"source/#tests.test_cli.test_fix","text":"Test dir1 fixture is not None. Source code in tests\\test_cli.py 34 35 36 37 38 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and dir2","title":"test_fix()"},{"location":"source/#tests.test_commands","text":"Testing functions for the sub-action commands from command line args.","title":"test_commands"},{"location":"source/#tests.test_commands.test_create_unicode_name","text":"Test Unicode information in CLI args. Source code in tests\\test_commands.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 def test_create_unicode_name ( file1 ): \"\"\" Test Unicode information in CLI args. \"\"\" parent = os . path . dirname ( file1 ) filename = os . path . join ( parent , \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" ) args = [ \"torrentfile\" , \"-v\" , \"create\" , \"-a\" , \"tracker_url.com/announce_3456\" , \"tracker_url.net/announce_3456\" , \"--source\" , \"sourcetext\" , \"--comment\" , \"filename is \u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"-o\" , str ( filename ), str ( file1 ), ] sys . argv = args execute () assert os . path . exists ( filename )","title":"test_create_unicode_name()"},{"location":"source/#tests.test_commands.test_fix","text":"Test dir1 fixture is not None. Source code in tests\\test_commands.py 37 38 39 40 41 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and metafile1 and file1 and metafile2 and dir2","title":"test_fix()"},{"location":"source/#tests.test_commands.test_info","text":"Test the info_command action from the Command Line Interface. Source code in tests\\test_commands.py 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 @pytest . mark . parametrize ( \"field\" , [ \"name\" , \"announce\" , \"source\" , \"comment\" , \"private\" , \"announce-list\" ], ) def test_info ( field , file1 ): \"\"\" Test the info_command action from the Command Line Interface. \"\"\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"url1\" , \"url2\" , \"url3\" , \"--web-seed\" , \"url4\" , \"url5\" , \"--http-seed\" , \"url6\" , \"url7\" , \"--private\" , \"--comment\" , \"ExampleComment\" , \"--source\" , \"examplesource\" , str ( file1 ), ] sys . argv = args execute () class Space : \"\"\" Stand in substitution for argparse.Namespace object. \"\"\" metafile = str ( file1 ) + \".torrent\" output = info ( Space ) assert field in output","title":"test_info()"},{"location":"source/#tests.test_commands.test_magnet","text":"Test create magnet function scheme. Source code in tests\\test_commands.py 65 66 67 68 69 70 def test_magnet ( metafile1 ): \"\"\" Test create magnet function scheme. \"\"\" magnet_link = magnet ( metafile1 ) assert magnet_link . startswith ( \"magnet\" )","title":"test_magnet()"},{"location":"source/#tests.test_commands.test_magnet_cli","text":"Test magnet creation through CLI interface. Source code in tests\\test_commands.py 136 137 138 139 140 141 142 def test_magnet_cli ( metafile1 ): \"\"\" Test magnet creation through CLI interface. \"\"\" sys . argv [ 1 :] = [ \"m\" , str ( metafile1 )] uri = execute () assert \"magnet\" in uri","title":"test_magnet_cli()"},{"location":"source/#tests.test_commands.test_magnet_empty","text":"Test create magnet function scheme. Source code in tests\\test_commands.py 84 85 86 87 88 89 90 91 def test_magnet_empty (): \"\"\" Test create magnet function scheme. \"\"\" try : magnet ( \"file_that_does_not_exist\" ) except FileNotFoundError : assert True","title":"test_magnet_empty()"},{"location":"source/#tests.test_commands.test_magnet_hex","text":"Test create magnet function digest. Source code in tests\\test_commands.py 54 55 56 57 58 59 60 61 62 def test_magnet_hex ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) info = meta [ \"info\" ] binfo = sha1 ( pyben . dumps ( info )) . hexdigest () . upper () assert binfo in magnet_link","title":"test_magnet_hex()"},{"location":"source/#tests.test_commands.test_magnet_no_announce_list","text":"Test create magnet function scheme. Source code in tests\\test_commands.py 73 74 75 76 77 78 79 80 81 def test_magnet_no_announce_list ( metafile2 ): \"\"\" Test create magnet function scheme. \"\"\" meta = pyben . load ( metafile2 ) del meta [ \"announce-list\" ] pyben . dump ( meta , metafile2 ) magnet_link = magnet ( metafile2 ) assert magnet_link . startswith ( \"magnet\" )","title":"test_magnet_no_announce_list()"},{"location":"source/#tests.test_commands.test_magnet_uri","text":"Test create magnet function digest. Source code in tests\\test_commands.py 44 45 46 47 48 49 50 51 def test_magnet_uri ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) announce = meta [ \"announce\" ] assert quote_plus ( announce ) in magnet_link","title":"test_magnet_uri()"},{"location":"source/#tests.test_commands.test_merkle_root_no_blocks","text":"Test running merkle root function with 1 and 0 len lists. Source code in tests\\test_commands.py 171 172 173 174 175 176 177 178 179 @pytest . mark . parametrize ( \"blocks\" , [[], [ sha1 ( b \"1010\" ) . digest ()]]) # nosec def test_merkle_root_no_blocks ( blocks ): \"\"\" Test running merkle root function with 1 and 0 len lists. \"\"\" if blocks : assert merkle_root ( blocks ) else : assert not merkle_root ( blocks )","title":"test_merkle_root_no_blocks()"},{"location":"source/#tests.test_commands.test_mixins_progbar","text":"Test progbar mixins with small file. Source code in tests\\test_commands.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 @pytest . mark . parametrize ( \"torrent\" , torrents ()) def test_mixins_progbar ( torrent ): \"\"\" Test progbar mixins with small file. \"\"\" tfile = tempfile ( exp = 14 ) msg = \"1234abcd\" * 80 with open ( tfile , \"wb\" ) as temp : temp . write ( msg . encode ( \"utf-8\" )) args = { \"path\" : tfile , \"--prog\" : \"1\" , } metafile = torrent ( ** args ) output , _ = metafile . write () assert output == str ( tfile ) + \".torrent\" rmpath ( tfile )","title":"test_mixins_progbar()"},{"location":"source/#tests.test_edit","text":"Testing the edit torrent feature.","title":"test_edit"},{"location":"source/#tests.test_edit.test_edit_cli","text":"Test edit torrent with all params on cli. Source code in tests\\test_edit.py 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 @pytest . mark . parametrize ( \"comment\" , [ \"commenta\" , \"commentb\" , \"commentc\" ]) @pytest . mark . parametrize ( \"source\" , [ \"sourcea\" , \"sourceb\" , \"sourcec\" ]) @pytest . mark . parametrize ( \"announce\" , [[ \"url1\" , \"url2\" , \"url3\" ], [ \"url1\" ]]) @pytest . mark . parametrize ( \"webseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) @pytest . mark . parametrize ( \"httpseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) def test_edit_cli ( metafile2 , comment , source , announce , webseed , httpseed ): \"\"\" Test edit torrent with all params on cli. \"\"\" sys . argv = [ \"torrentfile\" , \"edit\" , metafile2 , \"--comment\" , comment , \"--source\" , source , \"--web-seed\" , webseed , \"--http-seed\" , httpseed , \"--tracker\" , announce , \"--private\" , ] main () meta = pyben . load ( metafile2 ) info = meta [ \"info\" ] assert comment == info . get ( \"comment\" ) assert source == info . get ( \"source\" ) assert info . get ( \"private\" ) == 1 assert meta [ \"announce-list\" ] == [[ announce ]] assert meta [ \"url-list\" ] == [ webseed ]","title":"test_edit_cli()"},{"location":"source/#tests.test_edit.test_edit_comment","text":"Test edit torrent with comment param. Source code in tests\\test_edit.py 118 119 120 121 122 123 124 125 126 127 @pytest . mark . parametrize ( \"comment\" , [ \"COMMENT\" , \"COMIT\" , \"MITCO\" ]) def test_edit_comment ( metafile2 , comment ): \"\"\" Test edit torrent with comment param. \"\"\" edits = { \"comment\" : comment } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"comment\" ] == comment","title":"test_edit_comment()"},{"location":"source/#tests.test_edit.test_edit_httpseeds","text":"Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 104 105 106 107 108 109 110 111 112 113 114 115 @pytest . mark . parametrize ( \"httpseed\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_httpseeds ( metafile2 , httpseed ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"httpseeds\" : httpseed } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseed","title":"test_edit_httpseeds()"},{"location":"source/#tests.test_edit.test_edit_httpseeds_str","text":"Test edit torrent with webseed param. Source code in tests\\test_edit.py 78 79 80 81 82 83 84 85 86 87 @pytest . mark . parametrize ( \"httpseeds\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_httpseeds_str ( metafile2 , httpseeds ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"httpseeds\" : httpseeds } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseeds . split ()","title":"test_edit_httpseeds_str()"},{"location":"source/#tests.test_edit.test_edit_none","text":"Test edit torrent with None for all params. Source code in tests\\test_edit.py 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 def test_edit_none ( metafile2 ): \"\"\" Test edit torrent with None for all params. \"\"\" edits = { \"announce\" : None , \"url-list\" : None , \"comment\" : None , \"source\" : None , \"private\" : None , } data = pyben . load ( metafile2 ) edited = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta == edited","title":"test_edit_none()"},{"location":"source/#tests.test_edit.test_edit_private_false","text":"Test edit torrent with private param False. Source code in tests\\test_edit.py 153 154 155 156 157 158 159 160 161 def test_edit_private_false ( metafile2 ): \"\"\" Test edit torrent with private param False. \"\"\" edits = { \"private\" : \"\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert \"private\" not in data [ \"info\" ]","title":"test_edit_private_false()"},{"location":"source/#tests.test_edit.test_edit_private_true","text":"Test edit torrent with private param. Source code in tests\\test_edit.py 142 143 144 145 146 147 148 149 150 def test_edit_private_true ( metafile2 ): \"\"\" Test edit torrent with private param. \"\"\" edits = { \"private\" : \"1\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"private\" ] == 1","title":"test_edit_private_true()"},{"location":"source/#tests.test_edit.test_edit_removal","text":"Test edit torrent with empty for all params. Source code in tests\\test_edit.py 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_edit_removal ( metafile2 ): \"\"\" Test edit torrent with empty for all params. \"\"\" edits = { \"announce\" : \"\" , \"url-list\" : \"\" , \"httpseeds\" : \"\" , \"comment\" : \"\" , \"source\" : \"\" , \"private\" : \"\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta","title":"test_edit_removal()"},{"location":"source/#tests.test_edit.test_edit_source","text":"Test edit torrent with source param. Source code in tests\\test_edit.py 130 131 132 133 134 135 136 137 138 139 @pytest . mark . parametrize ( \"source\" , [ \"SomeSource\" , \"NoSouce\" , \"MidSource\" ]) def test_edit_source ( metafile2 , source ): \"\"\" Test edit torrent with source param. \"\"\" edits = { \"source\" : source } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"source\" ] == source","title":"test_edit_source()"},{"location":"source/#tests.test_edit.test_edit_torrent","text":"Test edit torrent with announce param. Source code in tests\\test_edit.py 40 41 42 43 44 45 46 47 48 49 50 51 @pytest . mark . parametrize ( \"announce\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_torrent ( metafile2 , announce ): \"\"\" Test edit torrent with announce param. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce ]","title":"test_edit_torrent()"},{"location":"source/#tests.test_edit.test_edit_torrent_str","text":"Test edit torrent with announce param as string. Source code in tests\\test_edit.py 54 55 56 57 58 59 60 61 62 63 @pytest . mark . parametrize ( \"announce\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_torrent_str ( metafile2 , announce ): \"\"\" Test edit torrent with announce param as string. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce . split ()]","title":"test_edit_torrent_str()"},{"location":"source/#tests.test_edit.test_edit_urllist","text":"Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 90 91 92 93 94 95 96 97 98 99 100 101 @pytest . mark . parametrize ( \"url_list\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_urllist ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list","title":"test_edit_urllist()"},{"location":"source/#tests.test_edit.test_edit_urllist_str","text":"Test edit torrent with webseed param. Source code in tests\\test_edit.py 66 67 68 69 70 71 72 73 74 75 @pytest . mark . parametrize ( \"url_list\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_urllist_str ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list . split ()","title":"test_edit_urllist_str()"},{"location":"source/#tests.test_edit.test_fix","text":"Testing dir fixtures. Source code in tests\\test_edit.py 33 34 35 36 37 def test_fix (): \"\"\" Testing dir fixtures. \"\"\" assert dir2 and metafile2 and dir1","title":"test_fix()"},{"location":"source/#tests.test_edit.test_metafile_edit_with_unicode","text":"Test if editing full unicode works as it should. Source code in tests\\test_edit.py 233 234 235 236 237 238 239 240 241 242 243 244 245 246 def test_metafile_edit_with_unicode ( metafile2 ): \"\"\" Test if editing full unicode works as it should. \"\"\" edits = { \"comment\" : \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"source\" : \"\u4e02\u4e03\u4e07\u4e0f\u4e11\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) com1 = data [ \"info\" ][ \"comment\" ] com2 = meta [ \"info\" ][ \"comment\" ] msg = edits [ \"comment\" ] assert com1 == com2 == msg","title":"test_metafile_edit_with_unicode()"},{"location":"source/#tests.test_interactive","text":"Testing functions for the command line interface.","title":"test_interactive"},{"location":"source/#tests.test_interactive.test_fixtures","text":"Test the fixtures used in module. Source code in tests\\test_interactive.py 37 38 39 40 41 def test_fixtures (): \"\"\" Test the fixtures used in module. \"\"\" assert filemeta2 and file1 and file2","title":"test_fixtures()"},{"location":"source/#tests.test_interactive.test_inter_create_full","text":"Test creating torrent interactively with many parameters. Source code in tests\\test_interactive.py 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"piece_length\" , [ \"23\" , \"18\" , \"131072\" ]) @pytest . mark . parametrize ( \"announce\" , [ \"url1\" , \"urla urlb urlc\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Do\" , \"Ra\" , \"Me\" ]) def test_inter_create_full ( file1 , piece_length , announce , comment , source , url_list , version , monkeypatch , ): \"\"\" Test creating torrent interactively with many parameters. \"\"\" mapping = [ \"create\" , piece_length , announce , url_list , url_list , comment , source , \"Y\" , file1 , str ( file1 ) + \".torrent\" , version , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta = pyben . load ( str ( file1 ) + \".torrent\" ) assert meta [ \"info\" ][ \"source\" ] == source assert meta [ \"info\" ][ \"piece length\" ] == normalize_piece_length ( piece_length ) assert meta [ \"info\" ][ \"comment\" ] == comment assert meta [ \"url-list\" ] == url_list . split ()","title":"test_inter_create_full()"},{"location":"source/#tests.test_interactive.test_inter_edit_cli","text":"Test editing torrent interactively from CLI. Source code in tests\\test_interactive.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 @pytest . mark . parametrize ( \"announce\" , [ \"urla urlb urlc\" , \"urld url2\" ]) @pytest . mark . parametrize ( \"urllist\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"cmnt\" , [ \"Some Comment\" ]) @pytest . mark . parametrize ( \"srce\" , [ \"Do\" , \"Ra\" ]) def test_inter_edit_cli ( filemeta2 , announce , cmnt , srce , urllist , monkeypatch ): \"\"\" Test editing torrent interactively from CLI. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , cmnt , \"2\" , srce , \"5\" , urllist , urllist , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) sys . argv = [ \"torrentfile\" , \"-i\" ] main () meta2 = pyben . load ( filemeta2 ) assert meta2 [ \"info\" ][ \"source\" ] == srce assert meta2 [ \"info\" ][ \"comment\" ] == cmnt assert meta2 [ \"url-list\" ] == urllist . split () assert meta2 [ \"info\" ][ \"private\" ] == 1","title":"test_inter_edit_cli()"},{"location":"source/#tests.test_interactive.test_inter_edit_full","text":"Test editing torrent file interactively. Source code in tests\\test_interactive.py 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 @pytest . mark . parametrize ( \"announce\" , [ \"url1\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Fa\" , \"So\" , \"La\" ]) def test_inter_edit_full ( filemeta2 , announce , comment , source , url_list , monkeypatch ): \"\"\" Test editing torrent file interactively. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , comment , \"2\" , source , \"5\" , url_list , \"\" , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta1 = pyben . load ( filemeta2 ) assert meta1 [ \"info\" ][ \"source\" ] == source assert meta1 [ \"info\" ][ \"comment\" ] == comment assert meta1 [ \"url-list\" ] == url_list . split () assert meta1 [ \"info\" ][ \"private\" ] == 1","title":"test_inter_edit_full()"},{"location":"source/#tests.test_interactive.test_inter_recheck","text":"Test interactive recheck function. Source code in tests\\test_interactive.py 180 181 182 183 184 185 186 187 188 189 190 191 @pytest . mark . parametrize ( \"torrentclass\" , torrents ()) def test_inter_recheck ( torrentclass , monkeypatch , file1 ): \"\"\" Test interactive recheck function. \"\"\" torrent = torrentclass ( path = file1 ) filemeta , _ = torrent . write () seq = [ \"recheck\" , filemeta , str ( file1 )] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) result = select_action () assert result == 100","title":"test_inter_recheck()"},{"location":"source/#tests.test_interactive.test_interactive_create","text":"Test creating torrent interactively. Source code in tests\\test_interactive.py 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 def test_interactive_create ( monkeypatch , file1 ): \"\"\" Test creating torrent interactively. \"\"\" mapping = [ \"create\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , file1 , str ( file1 ) + \".torrent\" , \"\" , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () assert os . path . exists ( str ( file1 ) + \".torrent\" )","title":"test_interactive_create()"},{"location":"source/#tests.test_recheck","text":"Testing functions for the progress module.","title":"test_recheck"},{"location":"source/#tests.test_recheck.test_checker_callback","text":"Test Checker class with directory that points to nothing. Source code in tests\\test_recheck.py 125 126 127 128 129 130 131 def test_checker_callback ( dir1 , metafile1 ): \"\"\" Test Checker class with directory that points to nothing. \"\"\" Checker . register_callback ( lambda * x : print ( x )) checker = Checker ( metafile1 , str ( dir1 )) assert checker . results () == 100","title":"test_checker_callback()"},{"location":"source/#tests.test_recheck.test_checker_class","text":"Test Checker Class against meta files. Source code in tests\\test_recheck.py 42 43 44 45 46 47 def test_checker_class ( dir1 , metafile1 ): \"\"\" Test Checker Class against meta files. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100","title":"test_checker_class()"},{"location":"source/#tests.test_recheck.test_checker_class_allfiles","text":"Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 def test_checker_class_allfiles ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" def traverse ( path ): \"\"\" Traverse internal subdirectories. \"\"\" if path . is_file (): rmpath ( path ) elif path . is_dir (): for item in path . iterdir (): traverse ( item ) traverse ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_class_allfiles()"},{"location":"source/#tests.test_recheck.test_checker_class_allpaths","text":"Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 211 212 213 214 215 216 217 218 def test_checker_class_allpaths ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" for item in Path ( str ( dir2 )) . iterdir (): rmpath ( item ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_class_allpaths()"},{"location":"source/#tests.test_recheck.test_checker_class_half_file","text":"Test Checker class with half size single file. Source code in tests\\test_recheck.py 221 222 223 224 225 226 227 228 229 230 231 232 def test_checker_class_half_file ( filemeta2 , file2 ): \"\"\" Test Checker class with half size single file. \"\"\" half = int ( os . path . getsize ( file2 ) / 2 ) barr = bytearray ( half ) with open ( file2 , \"rb\" ) as content : content . readinto ( barr ) with open ( file2 , \"wb\" ) as content : content . write ( barr ) checker = Checker ( filemeta2 , file2 ) assert int ( checker . results ()) != 10","title":"test_checker_class_half_file()"},{"location":"source/#tests.test_recheck.test_checker_cli_args","text":"Test exclusive Checker Mode CLI. Source code in tests\\test_recheck.py 134 135 136 137 138 139 140 def test_checker_cli_args ( dir1 , metafile1 ): \"\"\" Test exclusive Checker Mode CLI. \"\"\" sys . argv = [ \"torrentfile\" , \"check\" , str ( metafile1 ), str ( dir1 )] output = main () assert output == 100","title":"test_checker_cli_args()"},{"location":"source/#tests.test_recheck.test_checker_empty_files","text":"Test Checker when directory contains 0 length files. Source code in tests\\test_recheck.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 def test_checker_empty_files ( dir2 , sizedfiles ): \"\"\" Test Checker when directory contains 0 length files. \"\"\" def empty_files ( root ): \"\"\" Dump contents of files. \"\"\" if os . path . isfile ( root ): with open ( root , \"wb\" ) as _ : pass assert os . path . getsize ( root ) == 0 elif os . path . isdir ( root ): for item in os . listdir ( root ): return empty_files ( os . path . join ( root , item )) return root empty_files ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100","title":"test_checker_empty_files()"},{"location":"source/#tests.test_recheck.test_checker_first_piece","text":"Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 def test_checker_first_piece ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if path . is_file (): new = b \"Something other than what was there before.\" with open ( path , \"rb\" ) as bfile : data = bfile . read () new_len = len ( new ) content = b \"\" . join ([ new , data [ new_len :]]) with open ( path , \"wb\" ) as bdoc : bdoc . write ( content ) elif path . is_dir (): for item in path . iterdir (): change ( item ) change ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100","title":"test_checker_first_piece()"},{"location":"source/#tests.test_recheck.test_checker_first_piece_alt","text":"Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 def test_checker_first_piece_alt ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if os . path . isfile ( path ): with open ( path , \"rb\" ) as bfile : data = bfile . read () new = b \"some_other_bytes_to_use\" new_len = len ( new ) with open ( path , \"wb\" ) as wfile : wfile . write ( new + data [ new_len :]) elif os . path . isdir ( path ): for item in os . listdir ( path ): change ( os . path . join ( path , item )) change ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100","title":"test_checker_first_piece_alt()"},{"location":"source/#tests.test_recheck.test_checker_missing","text":"Test Checker class when files are missing from contents. Source code in tests\\test_recheck.py 179 180 181 182 183 184 185 186 187 188 def test_checker_missing ( sizedfiles , dir2 ): \"\"\" Test Checker class when files are missing from contents. \"\"\" count = 0 for fd in Path ( dir2 ) . iterdir (): if fd . is_file () and count < 2 : rmpath ( fd ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_missing()"},{"location":"source/#tests.test_recheck.test_checker_missing_singles","text":"Test Checker class with half size single file. Source code in tests\\test_recheck.py 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def test_checker_missing_singles ( dir2 , sizedfiles ): \"\"\" Test Checker class with half size single file. \"\"\" def walk ( root ): \"\"\" Remove first file found. \"\"\" if root . is_file (): rmpath ( root ) return True if root . is_dir (): for item in root . iterdir (): walk ( item ) return False walk ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_missing_singles()"},{"location":"source/#tests.test_recheck.test_checker_no_meta_file","text":"Test Checker when incorrect metafile is provided. Source code in tests\\test_recheck.py 159 160 161 162 163 164 165 166 def test_checker_no_meta_file (): \"\"\" Test Checker when incorrect metafile is provided. \"\"\" try : Checker ( \"peaches\" , \"$\" ) except FileNotFoundError : assert True","title":"test_checker_no_meta_file()"},{"location":"source/#tests.test_recheck.test_checker_parent_dir","text":"Test providing the parent directory for torrent checking feature. Source code in tests\\test_recheck.py 143 144 145 146 147 148 def test_checker_parent_dir ( dir1 , metafile1 ): \"\"\" Test providing the parent directory for torrent checking feature. \"\"\" checker = Checker ( metafile1 , os . path . dirname ( dir1 )) assert checker . results () == 100","title":"test_checker_parent_dir()"},{"location":"source/#tests.test_recheck.test_checker_result_property","text":"Test Checker class with half size single file. Source code in tests\\test_recheck.py 257 258 259 260 261 262 263 def test_checker_result_property ( dir1 , metafile1 ): \"\"\" Test Checker class with half size single file. \"\"\" checker = Checker ( metafile1 , dir1 ) result = checker . results () assert checker . results () == result","title":"test_checker_result_property()"},{"location":"source/#tests.test_recheck.test_checker_simplest","text":"Test the simplest example. Source code in tests\\test_recheck.py 266 267 268 269 270 271 def test_checker_simplest ( dir1 , metafile1 ): \"\"\" Test the simplest example. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100","title":"test_checker_simplest()"},{"location":"source/#tests.test_recheck.test_checker_with_file","text":"Test checker with single file torrent. Source code in tests\\test_recheck.py 151 152 153 154 155 156 def test_checker_with_file ( file1 , filemeta1 ): \"\"\" Test checker with single file torrent. \"\"\" checker = Checker ( filemeta1 , file1 ) assert checker . results () == 100","title":"test_checker_with_file()"},{"location":"source/#tests.test_recheck.test_checker_wrong_root_dir","text":"Test Checker when incorrect root directory is provided. Source code in tests\\test_recheck.py 169 170 171 172 173 174 175 176 def test_checker_wrong_root_dir ( metafile1 ): \"\"\" Test Checker when incorrect root directory is provided. \"\"\" try : Checker ( metafile1 , \"fake\" ) except FileNotFoundError : assert True","title":"test_checker_wrong_root_dir()"},{"location":"source/#tests.test_recheck.test_fixtures","text":"Test fixtures exist. Source code in tests\\test_recheck.py 33 34 35 36 37 38 39 def test_fixtures (): \"\"\" Test fixtures exist. \"\"\" assert dir1 and dir2 and file1 and file2 assert filemeta1 and filemeta2 and metafile1 assert metafile2 and sizes and sizedfiles","title":"test_fixtures()"},{"location":"source/#tests.test_recheck.test_partial_metafiles","text":"Test Checker with data that is expected to be incomplete. Source code in tests\\test_recheck.py 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 def test_partial_metafiles ( dir2 , sizedfiles ): \"\"\" Test Checker with data that is expected to be incomplete. \"\"\" def shortenfile ( path ): \"\"\" Shorten a few files for testing purposes. \"\"\" with open ( path , \"rb\" ) as bfile : data = bfile . read () with open ( path , \"wb\" ) as bfile : bfile . write ( data [: - ( 2 ** 10 )]) for item in os . listdir ( dir2 ): full = os . path . join ( dir2 , item ) if os . path . isfile ( full ): shortenfile ( full ) testdir = os . path . dirname ( dir2 ) checker = Checker ( sizedfiles , testdir ) assert checker . results () != 100","title":"test_partial_metafiles()"},{"location":"source/#tests.test_recheck.test_recheck_wrong_dir","text":"Test recheck function with directory that doesn't contain the contents. Source code in tests\\test_recheck.py 297 298 299 300 301 302 303 304 305 def test_recheck_wrong_dir ( metafile1 ): \"\"\" Test recheck function with directory that doesn't contain the contents. \"\"\" grandparent = os . path . dirname ( os . path . dirname ( metafile1 )) try : _ = Checker ( metafile1 , grandparent ) except FileNotFoundError : assert True","title":"test_recheck_wrong_dir()"},{"location":"source/#tests.test_torrent","text":"Testing functions for the torrent module.","title":"test_torrent"},{"location":"source/#tests.test_torrent.test_create_cwd_fail","text":"Test cwd argument with create command failure. Source code in tests\\test_torrent.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_create_cwd_fail (): \"\"\"Test cwd argument with create command failure.\"\"\" class SuFile : \"\"\"A mock admin file.\"\"\" @staticmethod def __fspath__ (): raise PermissionError def __str__ ( self ): return \"SuFile\" tfile = tempfile () name = os . path . basename ( tfile ) + \".torrent\" torrent = MetaFile ( path = tfile ) sufile = SuFile () torrent . write ( outfile = sufile ) current = os . path . join ( \".\" , name ) assert os . path . exists ( current ) rmpath ( tfile , current )","title":"test_create_cwd_fail()"},{"location":"source/#tests.test_torrent.test_fixtures","text":"Test pytest fixtures. Source code in tests\\test_torrent.py 32 33 34 35 36 def test_fixtures (): \"\"\" Test pytest fixtures. \"\"\" assert dir1 and dir2","title":"test_fixtures()"},{"location":"source/#tests.test_torrent.test_mbtorrent","text":"Test torrent creation for file size larger than 10MB. Source code in tests\\test_torrent.py 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 @pytest . mark . parametrize ( \"version\" , torrents ()) @pytest . mark . parametrize ( \"progress\" , [ 0 , 1 , 2 ]) def test_mbtorrent ( version , progress ): \"\"\" Test torrent creation for file size larger than 10MB. \"\"\" tfile = tempfile ( exp = 26 ) args = { \"path\" : tfile , \"progress\" : progress , \"piece_length\" : \"14\" , } torrent = version ( ** args ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile )","title":"test_mbtorrent()"},{"location":"source/#tests.test_torrent.test_metafile_assemble","text":"Test assembling base metafile exception. Source code in tests\\test_torrent.py 50 51 52 53 54 55 56 57 58 def test_metafile_assemble ( dir1 ): \"\"\" Test assembling base metafile exception. \"\"\" metafile = MetaFile ( path = dir1 ) try : metafile . assemble () except NotImplementedError : assert True","title":"test_metafile_assemble()"},{"location":"source/#tests.test_torrent.test_torrentfile_extra","text":"Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_extra ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" def walk ( item ): \"\"\" Edit files in directory structure. \"\"\" if item . is_file (): with open ( item , \"ab\" ) as binfile : binfile . write ( bytes ( 1000 )) elif item . is_dir (): for sub in item . iterdir (): walk ( sub ) walk ( dir2 ) args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\"","title":"test_torrentfile_extra()"},{"location":"source/#tests.test_torrent.test_torrentfile_missing_path","text":"Test missing path error exception. Source code in tests\\test_torrent.py 39 40 41 42 43 44 45 46 47 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_missing_path ( version ): \"\"\" Test missing path error exception. \"\"\" try : version () except MissingPathError : assert True","title":"test_torrentfile_missing_path()"},{"location":"source/#tests.test_torrent.test_torrentfile_one_empty","text":"Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_one_empty ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" a = next ( os . walk ( dir2 )) if len ( a [ - 1 ]) > 0 : with open ( os . path . join ( a [ 0 ], a [ - 1 ][ 0 ]), \"w\" , encoding = \"utf-8\" ) as _ : pass args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\"","title":"test_torrentfile_one_empty()"},{"location":"source/#tests.test_torrent.test_torrentfile_single","text":"Test creating a torrent file from a single file contents. Source code in tests\\test_torrent.py 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 @pytest . mark . parametrize ( \"num\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single ( version , num , piece_length , capsys ): \"\"\" Test creating a torrent file from a single file contents. \"\"\" tfile = tempfile ( exp = num ) with capsys . disabled (): version . set_callback ( print ) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } trent = version ( ** args ) trent . write () assert os . path . exists ( str ( tfile ) + \".torrent\" ) rmpath ( tfile , str ( tfile ) + \".torrent\" )","title":"test_torrentfile_single()"},{"location":"source/#tests.test_torrent.test_torrentfile_single_extra","text":"Test creating a torrent file from a single file contents plus extra. Source code in tests\\test_torrent.py 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 @pytest . mark . parametrize ( \"size\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single_extra ( version , size , piece_length ): \"\"\" Test creating a torrent file from a single file contents plus extra. \"\"\" tfile = tempfile ( exp = size ) with open ( tfile , \"ab\" ) as binfile : binfile . write ( bytes ( str ( tfile ) . encode ( \"utf-8\" ))) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } torrent = version ( ** args ) torrent . write () outfile = str ( tfile ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( tfile , outfile )","title":"test_torrentfile_single_extra()"},{"location":"source/#tests.test_torrent.test_torrentfile_single_under","text":"Test creating a torrent file from less than a single file contents. Source code in tests\\test_torrent.py 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 @pytest . mark . parametrize ( \"sze\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piecelength\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"ver\" , torrents ()) def test_torrentfile_single_under ( ver , sze , piecelength ): \"\"\" Test creating a torrent file from less than a single file contents. \"\"\" tfile = tempfile ( exp = sze ) with open ( tfile , \"rb\" ) as binfile : data = binfile . read () with open ( tfile , \"wb\" ) as binfile : binfile . write ( data [: - ( 2 ** 9 )]) kwargs = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piecelength , } torrent = ver ( ** kwargs ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile )","title":"test_torrentfile_single_under()"},{"location":"source/#tests.test_torrent.test_waiting_mixin","text":"Test waiting function. Source code in tests\\test_torrent.py 198 199 200 201 202 203 204 205 206 def test_waiting_mixin (): \"\"\" Test waiting function. \"\"\" msg = \"Testing message\" lst = [] timeout = 3 waiting ( msg , lst , timeout = timeout ) assert len ( lst ) == 0","title":"test_waiting_mixin()"},{"location":"source/#tests.test_utils","text":"Unittest functions for testing torrentfile utils module.","title":"test_utils"},{"location":"source/#tests.test_utils.test_filelist_total","text":"Test function for acquiring a filelist for directory. Source code in tests\\test_utils.py 118 119 120 121 122 123 def test_filelist_total ( dir1 ): \"\"\" Test function for acquiring a filelist for directory. \"\"\" total , _ = utils . filelist_total ( dir1 ) assert total == ( 2 ** 18 ) * 8","title":"test_filelist_total()"},{"location":"source/#tests.test_utils.test_filelisttotal_missing","text":"Test function filelist total with missing path. Parameters: Name Type Description Default dir2 pytest . fixture fixture containing a temporary directory required Source code in tests\\test_utils.py 225 226 227 228 229 230 231 232 233 234 235 236 237 def test_filelisttotal_missing ( dir2 ): \"\"\"Test function filelist total with missing path. Parameters ---------- dir2 : pytest.fixture fixture containing a temporary directory \"\"\" rmpath ( dir2 ) try : utils . filelist_total ( dir2 ) except utils . MissingPathError : assert True","title":"test_filelisttotal_missing()"},{"location":"source/#tests.test_utils.test_get_filelist","text":"Test function for get a list of files in a directory. Source code in tests\\test_utils.py 102 103 104 105 106 107 def test_get_filelist ( dir1 ): \"\"\" Test function for get a list of files in a directory. \"\"\" filelist = utils . get_file_list ( dir1 ) assert len ( filelist ) == 8","title":"test_get_filelist()"},{"location":"source/#tests.test_utils.test_get_path_length_max","text":"Test function for getting piece length for folders max. Source code in tests\\test_utils.py 71 72 73 74 75 def test_get_path_length_max ( dir1 ): \"\"\" Test function for getting piece length for folders max. \"\"\" assert utils . path_piece_length ( dir1 ) <= ( 2 ** 27 )","title":"test_get_path_length_max()"},{"location":"source/#tests.test_utils.test_get_path_length_min","text":"Test function for getting piece length for folders min. Source code in tests\\test_utils.py 64 65 66 67 68 def test_get_path_length_min ( dir1 ): \"\"\" Test function for getting piece length for folders min. \"\"\" assert utils . path_piece_length ( dir1 ) >= ( 2 ** 14 )","title":"test_get_path_length_min()"},{"location":"source/#tests.test_utils.test_get_path_length_mod","text":"Test function for the best piece length for provided path. Source code in tests\\test_utils.py 57 58 59 60 61 def test_get_path_length_mod ( dir1 ): \"\"\" Test function for the best piece length for provided path. \"\"\" assert utils . path_piece_length ( dir1 ) % ( 2 ** 14 ) == 0","title":"test_get_path_length_mod()"},{"location":"source/#tests.test_utils.test_get_path_size","text":"Test function for getting total size of directory. Source code in tests\\test_utils.py 110 111 112 113 114 115 def test_get_path_size ( dir1 ): \"\"\" Test function for getting total size of directory. \"\"\" pathsize = utils . path_size ( dir1 ) assert pathsize == ( 2 ** 18 ) * 8","title":"test_get_path_size()"},{"location":"source/#tests.test_utils.test_get_piece_length","text":"Test function for best piece length for given size. Source code in tests\\test_utils.py 30 31 32 33 34 35 36 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length ( size ): \"\"\" Test function for best piece length for given size. \"\"\" value = utils . get_piece_length ( size ) assert value % 1024 == 0","title":"test_get_piece_length()"},{"location":"source/#tests.test_utils.test_get_piece_length_max","text":"Test function for best piece length for given size maximum. Source code in tests\\test_utils.py 39 40 41 42 43 44 45 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_max ( size ): \"\"\" Test function for best piece length for given size maximum. \"\"\" value = utils . get_piece_length ( size ) assert value < 2 ** 27","title":"test_get_piece_length_max()"},{"location":"source/#tests.test_utils.test_get_piece_length_min","text":"Test function for best piece length for given size minimum. Source code in tests\\test_utils.py 48 49 50 51 52 53 54 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_min ( size ): \"\"\" Test function for best piece length for given size minimum. \"\"\" value = utils . get_piece_length ( size ) assert value >= 2 ** 14","title":"test_get_piece_length_min()"},{"location":"source/#tests.test_utils.test_humanize_bytes","text":"Test humanize bytes function. Source code in tests\\test_utils.py 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 @pytest . mark . parametrize ( \"amount, result\" , [ ( 100 , \"100\" ), ( 1100 , \"1 KiB\" ), ( 1_100_000 , \"1 MiB\" ), ( 1_100_000_000 , \"1 GiB\" ), ( 4_400_120_000 , \"4 GiB\" ), ( 4_000_120_000 , \"3 GiB\" ), ], ) def test_humanize_bytes ( amount , result ): \"\"\" Test humanize bytes function. \"\"\" assert utils . humanize_bytes ( amount ) == result","title":"test_humanize_bytes()"},{"location":"source/#tests.test_utils.test_missing_path_error","text":"Test exception for missing path parameter. Source code in tests\\test_utils.py 137 138 139 140 141 142 143 144 145 def test_missing_path_error (): \"\"\" Test exception for missing path parameter. \"\"\" try : raise utils . MissingPathError ( \"message\" ) except utils . MissingPathError : assert True assert dir2","title":"test_missing_path_error()"},{"location":"source/#tests.test_utils.test_next_power_2","text":"Test next power of 2 function in utils module. Source code in tests\\test_utils.py 148 149 150 151 152 153 154 155 156 157 @pytest . mark . parametrize ( \"value\" , [ 5 , 32 , 18 , 225 , 16384 , 256000 ]) def test_next_power_2 ( value ): \"\"\" Test next power of 2 function in utils module. \"\"\" result = utils . next_power_2 ( value ) log = math . log2 ( result ) assert log == int ( log ) assert result % 2 == 0 assert result >= value","title":"test_next_power_2()"},{"location":"source/#tests.test_utils.test_norm_plength_errors","text":"Test function to normalize piece length errors. Parameters: Name Type Description Default amount any arguments intended to raise an exception. required Source code in tests\\test_utils.py 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"amount\" , [ \"hello\" , 11 , 0 , 100000 , 28 , \"zero\" , \"fifteen\" ] ) def test_norm_plength_errors ( amount ): \"\"\"Test function to normalize piece length errors. Parameters ---------- amount : any arguments intended to raise an exception. \"\"\" try : assert utils . normalize_piece_length ( amount ) except utils . PieceLengthValueError : assert True","title":"test_norm_plength_errors()"},{"location":"source/#tests.test_utils.test_normalize_piece_length_int","text":"Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 178 179 180 181 182 183 184 185 186 187 188 189 @pytest . mark . parametrize ( \"amount, result\" , [( i , 2 ** i ) for i in range ( 14 , 25 )]) def test_normalize_piece_length_int ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result","title":"test_normalize_piece_length_int()"},{"location":"source/#tests.test_utils.test_normalize_piece_length_str","text":"Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 192 193 194 195 196 197 198 199 200 201 202 203 204 205 @pytest . mark . parametrize ( \"amount, result\" , [( str ( i ), 2 ** i ) for i in range ( 14 , 21 )] ) def test_normalize_piece_length_str ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result","title":"test_normalize_piece_length_str()"},{"location":"source/#tests.test_utils.test_path_stat","text":"Test function for acquiring piece length information on folder. Source code in tests\\test_utils.py 78 79 80 81 82 83 def test_path_stat ( dir1 ): \"\"\" Test function for acquiring piece length information on folder. \"\"\" _ , _ , piece_length = utils . path_stat ( dir1 ) assert piece_length % ( 2 ** 14 ) == 0","title":"test_path_stat()"},{"location":"source/#tests.test_utils.test_path_stat_filelist_size","text":"Test function for acquiring file list information on folder. Source code in tests\\test_utils.py 94 95 96 97 98 99 def test_path_stat_filelist_size ( dir1 ): \"\"\" Test function for acquiring file list information on folder. \"\"\" filelist , _ , _ = utils . path_stat ( dir1 ) assert len ( filelist ) == 8","title":"test_path_stat_filelist_size()"},{"location":"source/#tests.test_utils.test_path_stat_size","text":"Test function for acquiring total size information on folder. Source code in tests\\test_utils.py 86 87 88 89 90 91 def test_path_stat_size ( dir1 ): \"\"\" Test function for acquiring total size information on folder. \"\"\" _ , totalsize , _ = utils . path_stat ( dir1 ) assert totalsize == ( 2 ** 18 ) * 8","title":"test_path_stat_size()"},{"location":"source/#tests.test_utils.test_piecelength_error_fixtures","text":"Test exception for uninterpretable piece length value. Source code in tests\\test_utils.py 126 127 128 129 130 131 132 133 134 def test_piecelength_error_fixtures (): \"\"\" Test exception for uninterpretable piece length value. \"\"\" try : raise utils . PieceLengthValueError ( \"message\" ) except utils . PieceLengthValueError : assert True assert dir1","title":"test_piecelength_error_fixtures()"}]} \ No newline at end of file diff --git a/docs/sitemap.xml.gz b/docs/sitemap.xml.gz index a9bdacfbcb941c10d25080de4f3b03f20b0e277c..ece310b3bac931a200b8df6562e294181637f706 100644 GIT binary patch delta 14 VcmZo*YG7iM@8;mx`)nfHF901=1o;2} delta 14 VcmZo*YG7iM@8;mR|9B$XF903!1sDJT diff --git a/docs/source/index.html b/docs/source/index.html index 1d54efbf..fba17d53 100644 --- a/docs/source/index.html +++ b/docs/source/index.html @@ -2279,7 +2279,7 @@

213 214 215 -216
def magnet(metafile):
+216
def magnet(metafile: str):
     """
     Create a magnet URI from a Bittorrent meta file.
 
@@ -2374,11 +2374,11 @@ 

Source code in torrentfile\__main__.py -
25
-26
+          
26
 27
 28
-29
def main():
+29
+30
def main():
     """
     Start the entry point script.
     """
@@ -4580,7 +4580,7 @@ 

213 214 215 -216

def magnet(metafile):
+216
def magnet(metafile: str):
     """
     Create a magnet URI from a Bittorrent meta file.
 
@@ -4714,7 +4714,7 @@ 

169 170 171 -172

def recheck(args):
+172
def recheck(args: list):
     """
     Execute recheck CLI sub-command.
 
@@ -5350,12 +5350,7 @@ 

531 532 533 -534 -535 -536 -537 -538 -539

class FileHasher(CbMixin, ProgMixin):
+534
class FileHasher(CbMixin, ProgMixin):
     """
     Calculate root and piece hashes for creating hybrid torrent file.
 
@@ -5428,11 +5423,6 @@ 

""" Calculate layer hashes for contents of file. - Parameters - ---------- - data : BytesIO - File opened in read mode. - Returns ------- bytes @@ -5644,30 +5634,6 @@

Calculate layer hashes for contents of file.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
data - BytesIO -

File opened in read mode.

- required -
-

Returns:

@@ -5737,20 +5703,10 @@
515 516 517 -518 -519 -520 -521 -522 -523
def __next__(self) -> bytes:
+518
def __next__(self) -> bytes:
     """
     Calculate layer hashes for contents of file.
 
-    Parameters
-    ----------
-    data : BytesIO
-        File opened in read mode.
-
     Returns
     -------
     bytes
@@ -5823,7 +5779,12 @@ 
Source code in torrentfile\hasher.py -
525
+          
-          
+          
@@ -9560,7 +9516,7 @@ 

Parameters ---------- - args : tuple + *args : tuple Arbitrary number of args to pass to next function Returns @@ -11702,7 +11658,7 @@

Example
Parameters ---------- - args : dict + *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` @@ -12435,13 +12391,13 @@
- + @@ -12487,7 +12443,7 @@
Parameters ---------- - args : dict + *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` @@ -12882,16 +12838,6 @@

required

- - - - - -
520
+521
+522
+523
+524
+525
 526
 527
 528
@@ -5832,12 +5793,7 @@ 
531 532 533 -534 -535 -536 -537 -538 -539
def _calculate_root(self):
+534
def _calculate_root(self):
     """
     Calculate the root hash for opened file.
     """
@@ -9506,13 +9462,13 @@ 

args*args tuple

Arbitrary number of args to pass to next function

- required + ()
args*args dict

formatting args for log message

- required + ()
hasher - Any -

hashing class for calculating piece hashes. default=None

- required -
@@ -13050,9 +12996,7 @@

471 472 473 -474 -475 -476

class FeedChecker(ProgMixin):
+474
class FeedChecker(ProgMixin):
     """
     Validates torrent content.
 
@@ -13063,8 +13007,6 @@ 

---------- checker : object the checker class instance. - hasher : Any - hashing class for calculating piece hashes. default=None """ def __init__(self, checker: Checker): @@ -13247,7 +13189,9 @@

Source code in torrentfile\recheck.py -
337
+          
335
+336
+337
 338
 339
 340
@@ -13256,9 +13200,7 @@ 
343 344 345 -346 -347 -348
def __init__(self, checker: Checker):
+346
def __init__(self, checker: Checker):
     """
     Generate hashes of piece length data from filelist contents.
     """
@@ -13296,12 +13238,12 @@ 
Source code in torrentfile\recheck.py -
350
+          
348
+349
+350
 351
 352
-353
-354
-355
def __iter__(self):
+353
def __iter__(self):
     """
     Assign iterator and return self.
     """
@@ -13333,7 +13275,9 @@ 
Source code in torrentfile\recheck.py -
357
+          
355
+356
+357
 358
 359
 360
@@ -13346,9 +13290,7 @@ 
367 368 369 -370 -371 -372
def __next__(self):
+370
def __next__(self):
     """
     Yield back result of comparison.
     """
@@ -13452,7 +13394,9 @@ 
Source code in torrentfile\recheck.py -
447
+          
445
+446
+447
 448
 449
 450
@@ -13479,9 +13423,7 @@ 
471 472 473 -474 -475 -476
def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
+474
def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
     """
     Create padded pieces where file sizes do not match.
 
@@ -13589,7 +13531,9 @@ 
Source code in torrentfile\recheck.py -
407
+          
405
+406
+407
 408
 409
 410
@@ -13625,9 +13569,7 @@ 
440 441 442 -443 -444 -445
def extract(self, path: str, partial: bytearray) -> bytearray:
+443
def extract(self, path: str, partial: bytearray) -> bytearray:
     """
     Split file paths contents into blocks of data for hash pieces.
 
@@ -13710,7 +13652,9 @@ 
Source code in torrentfile\recheck.py -
374
+          
-        
-          
-          
-          
-          
-        
372
+373
+374
 375
 376
 377
@@ -13739,9 +13683,7 @@ 
400 401 402 -403 -404 -405
def iter_pieces(self):
+403
def iter_pieces(self):
     """
     Iterate through, and hash pieces of torrent contents.
 
@@ -13832,23 +13774,15 @@ 

required

hasher - Object -

the version specific hashing class for torrent content.

- required -
Source code in torrentfile\recheck.py -
479
+            
477
+478
+479
 480
 481
 482
@@ -14022,11 +13956,7 @@ 

650 651 652 -653 -654 -655 -656 -657

class HashChecker(ProgMixin):
+653
class HashChecker(ProgMixin):
     """
     Verify that root hashes of content files match the .torrent files.
 
@@ -14034,8 +13964,6 @@ 

---------- checker : Object the checker instance that maintains variables. - hasher : Object - the version specific hashing class for torrent content. """ def __init__(self, checker: Checker): @@ -14279,7 +14207,11 @@

Source code in torrentfile\recheck.py -
522
+            
518
+519
+520
+521
+522
 523
 524
 525
@@ -14328,11 +14260,7 @@ 
568 569 570 -571 -572 -573 -574 -575
class Padder:
+571
class Padder:
     """
     Padding class to generate padding hashes wherever needed.
 
@@ -14455,7 +14383,11 @@ 
Source code in torrentfile\recheck.py -
534
+          
530
+531
+532
+533
+534
 535
 536
 537
@@ -14464,11 +14396,7 @@ 
540 541 542 -543 -544 -545 -546 -547
def __init__(self, length, piece_length):
+543
def __init__(self, length, piece_length):
     """
     Construct padding class to Mock missing or incomplete files.
 
@@ -14508,11 +14436,11 @@ 
Source code in torrentfile\recheck.py -
549
-550
-551
-552
-553
def __iter__(self):
+          
545
+546
+547
+548
+549
def __iter__(self):
     """
     Return self to correctly implement iterator type.
     """
@@ -14579,7 +14507,11 @@ 
Source code in torrentfile\recheck.py -
555
+          
551
+552
+553
+554
+555
 556
 557
 558
@@ -14595,11 +14527,7 @@ 
568 569 570 -571 -572 -573 -574 -575
def __next__(self) -> bytes:
+571
def __next__(self) -> bytes:
     """
     Iterate through seemingly endless sha256 hashes of zeros.
 
@@ -14657,17 +14585,17 @@ 
Source code in torrentfile\recheck.py -
491
+          
487
+488
+489
+490
+491
 492
 493
 494
 495
 496
-497
-498
-499
-500
-501
def __init__(self, checker: Checker):
+497
def __init__(self, checker: Checker):
     """
     Construct a HybridChecker instance.
     """
@@ -14704,11 +14632,11 @@ 
Source code in torrentfile\recheck.py -
503
-504
-505
-506
-507
def __iter__(self):
+          
499
+500
+501
+502
+503
def __iter__(self):
     """
     Assign iterator and return self.
     """
@@ -14739,18 +14667,18 @@ 
Source code in torrentfile\recheck.py -
509
+          
505
+506
+507
+508
+509
 510
 511
 512
 513
 514
 515
-516
-517
-518
-519
-520
def __next__(self):
+516
def __next__(self):
     """
     Provide the result of comparison.
     """
@@ -14806,7 +14734,11 @@ 
Source code in torrentfile\recheck.py -
638
+          
634
+635
+636
+637
+638
 639
 640
 641
@@ -14821,11 +14753,7 @@ 
650 651 652 -653 -654 -655 -656 -657
def advance(self) -> tuple:
+653
def advance(self) -> tuple:
     """
     Increment the number of pieces processed for the current file.
 
@@ -14889,7 +14817,11 @@ 
Source code in torrentfile\recheck.py -
577
+          
573
+574
+575
+576
+577
 578
 579
 580
@@ -14917,11 +14849,7 @@ 
602 603 604 -605 -606 -607 -608 -609
def next_file(self) -> bool:
+605
def next_file(self) -> bool:
     """
     Remove all references to  processed files and prepare for the next.
 
@@ -15016,7 +14944,11 @@ 
Source code in torrentfile\recheck.py -
611
+          
-          
+          
@@ -16606,7 +16534,7 @@ 

Parameters ---------- - kwargs : dict + **kwargs : dict Keyword arguments for torrent options. """ @@ -17018,13 +16946,13 @@

- + @@ -17095,7 +17023,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict Dictionary containing torrent file options. """ @@ -17107,7 +17035,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict dictionary of keyword args passed to superclass. """ super().__init__(**kwargs) @@ -17185,13 +17113,13 @@

- + @@ -17216,7 +17144,7 @@
Parameters ---------- - kwargs : dict + **kwargs : dict dictionary of keyword args passed to superclass. """ super().__init__(**kwargs) @@ -17372,13 +17300,13 @@

- + @@ -17487,7 +17415,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict Keyword arguments for torrent options. """ @@ -17891,13 +17819,13 @@

- + @@ -17997,7 +17925,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict Keyword arguments for torrent file options. """ @@ -18011,7 +17939,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict keywword arguments to pass to superclass. """ super().__init__(**kwargs) @@ -18120,13 +18048,13 @@

- + @@ -18158,7 +18086,7 @@
Parameters ---------- - kwargs : dict + **kwargs : dict keywword arguments to pass to superclass. """ super().__init__(**kwargs) @@ -22496,12 +22424,7 @@

531 532 533 -534 -535 -536 -537 -538 -539

607
+608
+609
+610
+611
 612
 613
 614
@@ -15037,11 +14969,7 @@ 
629 630 631 -632 -633 -634 -635 -636
def process_current(self) -> tuple:
+632
def process_current(self) -> tuple:
     """
     Gather necessary information to compare to metafile details.
 
@@ -16486,13 +16414,13 @@ 

kwargs**kwargs dict

Keyword arguments for torrent options.

- required + {}
kwargs**kwargs dict

Dictionary containing torrent file options.

- required + {}
kwargs**kwargs dict

dictionary of keyword args passed to superclass.

- required + {}
kwargs**kwargs dict

Keyword arguments for torrent options.

- required + {}
kwargs**kwargs dict

Keyword arguments for torrent file options.

- required + {}
kwargs**kwargs dict

keywword arguments to pass to superclass.

- required + {}
class FileHasher(CbMixin, ProgMixin):
+534
class FileHasher(CbMixin, ProgMixin):
     """
     Calculate root and piece hashes for creating hybrid torrent file.
 
@@ -22574,11 +22497,6 @@ 

""" Calculate layer hashes for contents of file. - Parameters - ---------- - data : BytesIO - File opened in read mode. - Returns ------- bytes @@ -22790,30 +22708,6 @@

Calculate layer hashes for contents of file.

-

Parameters:

- - - - - - - - - - - - - - - - - -
NameTypeDescriptionDefault
data - BytesIO -

File opened in read mode.

- required -
-

Returns:

@@ -22883,20 +22777,10 @@

515 516 517 -518 -519 -520 -521 -522 -523

def __next__(self) -> bytes:
+518
def __next__(self) -> bytes:
     """
     Calculate layer hashes for contents of file.
 
-    Parameters
-    ----------
-    data : BytesIO
-        File opened in read mode.
-
     Returns
     -------
     bytes
@@ -22969,7 +22853,12 @@ 

Source code in torrentfile\hasher.py -
525
+          
-          
+          
@@ -26706,7 +26590,7 @@ 

Parameters ---------- - args : tuple + *args : tuple Arbitrary number of args to pass to next function Returns @@ -27481,7 +27365,7 @@

Example
Parameters ---------- - args : dict + *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` @@ -28214,13 +28098,13 @@

- + @@ -28266,7 +28150,7 @@

Parameters ---------- - args : dict + *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` @@ -28661,16 +28545,6 @@

required

- - - - - -
520
+521
+522
+523
+524
+525
 526
 527
 528
@@ -22978,12 +22867,7 @@ 

531 532 533 -534 -535 -536 -537 -538 -539

def _calculate_root(self):
+534
def _calculate_root(self):
     """
     Calculate the root hash for opened file.
     """
@@ -26652,13 +26536,13 @@ 

args*args tuple

Arbitrary number of args to pass to next function

- required + ()
args*args dict

formatting args for log message

- required + ()
hasher - Any -

hashing class for calculating piece hashes. default=None

- required -
@@ -28829,9 +28703,7 @@

471 472 473 -474 -475 -476

class FeedChecker(ProgMixin):
+474
class FeedChecker(ProgMixin):
     """
     Validates torrent content.
 
@@ -28842,8 +28714,6 @@ 

---------- checker : object the checker class instance. - hasher : Any - hashing class for calculating piece hashes. default=None """ def __init__(self, checker: Checker): @@ -29026,7 +28896,9 @@

Source code in torrentfile\recheck.py -
337
+          
335
+336
+337
 338
 339
 340
@@ -29035,9 +28907,7 @@ 

343 344 345 -346 -347 -348

def __init__(self, checker: Checker):
+346
def __init__(self, checker: Checker):
     """
     Generate hashes of piece length data from filelist contents.
     """
@@ -29075,12 +28945,12 @@ 

Source code in torrentfile\recheck.py -
350
+          
348
+349
+350
 351
 352
-353
-354
-355
def __iter__(self):
+353
def __iter__(self):
     """
     Assign iterator and return self.
     """
@@ -29112,7 +28982,9 @@ 

Source code in torrentfile\recheck.py -
357
+          
355
+356
+357
 358
 359
 360
@@ -29125,9 +28997,7 @@ 

367 368 369 -370 -371 -372

def __next__(self):
+370
def __next__(self):
     """
     Yield back result of comparison.
     """
@@ -29231,7 +29101,9 @@ 

Source code in torrentfile\recheck.py -
447
+          
445
+446
+447
 448
 449
 450
@@ -29258,9 +29130,7 @@ 

471 472 473 -474 -475 -476

def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
+474
def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
     """
     Create padded pieces where file sizes do not match.
 
@@ -29368,7 +29238,9 @@ 

Source code in torrentfile\recheck.py -
407
+          
405
+406
+407
 408
 409
 410
@@ -29404,9 +29276,7 @@ 

440 441 442 -443 -444 -445

def extract(self, path: str, partial: bytearray) -> bytearray:
+443
def extract(self, path: str, partial: bytearray) -> bytearray:
     """
     Split file paths contents into blocks of data for hash pieces.
 
@@ -29489,7 +29359,9 @@ 

Source code in torrentfile\recheck.py -
374
+          
-        
-          
-          
-          
-          
-        
372
+373
+374
 375
 376
 377
@@ -29518,9 +29390,7 @@ 

400 401 402 -403 -404 -405

def iter_pieces(self):
+403
def iter_pieces(self):
     """
     Iterate through, and hash pieces of torrent contents.
 
@@ -29611,23 +29481,15 @@ 

required

hasher - Object -

the version specific hashing class for torrent content.

- required -
Source code in torrentfile\recheck.py -
479
+            
477
+478
+479
 480
 481
 482
@@ -29801,11 +29663,7 @@ 

650 651 652 -653 -654 -655 -656 -657

class HashChecker(ProgMixin):
+653
class HashChecker(ProgMixin):
     """
     Verify that root hashes of content files match the .torrent files.
 
@@ -29813,8 +29671,6 @@ 

---------- checker : Object the checker instance that maintains variables. - hasher : Object - the version specific hashing class for torrent content. """ def __init__(self, checker: Checker): @@ -30058,7 +29914,11 @@

Source code in torrentfile\recheck.py -
522
+            
518
+519
+520
+521
+522
 523
 524
 525
@@ -30107,11 +29967,7 @@ 

568 569 570 -571 -572 -573 -574 -575

class Padder:
+571
class Padder:
     """
     Padding class to generate padding hashes wherever needed.
 
@@ -30234,7 +30090,11 @@ 
Source code in torrentfile\recheck.py -
534
+          
530
+531
+532
+533
+534
 535
 536
 537
@@ -30243,11 +30103,7 @@ 
540 541 542 -543 -544 -545 -546 -547
def __init__(self, length, piece_length):
+543
def __init__(self, length, piece_length):
     """
     Construct padding class to Mock missing or incomplete files.
 
@@ -30287,11 +30143,11 @@ 
Source code in torrentfile\recheck.py -
549
-550
-551
-552
-553
def __iter__(self):
+          
545
+546
+547
+548
+549
def __iter__(self):
     """
     Return self to correctly implement iterator type.
     """
@@ -30358,7 +30214,11 @@ 
Source code in torrentfile\recheck.py -
555
+          
551
+552
+553
+554
+555
 556
 557
 558
@@ -30374,11 +30234,7 @@ 
568 569 570 -571 -572 -573 -574 -575
def __next__(self) -> bytes:
+571
def __next__(self) -> bytes:
     """
     Iterate through seemingly endless sha256 hashes of zeros.
 
@@ -30436,17 +30292,17 @@ 

Source code in torrentfile\recheck.py -
491
+          
487
+488
+489
+490
+491
 492
 493
 494
 495
 496
-497
-498
-499
-500
-501
def __init__(self, checker: Checker):
+497
def __init__(self, checker: Checker):
     """
     Construct a HybridChecker instance.
     """
@@ -30483,11 +30339,11 @@ 

Source code in torrentfile\recheck.py -
503
-504
-505
-506
-507
def __iter__(self):
+          
499
+500
+501
+502
+503
def __iter__(self):
     """
     Assign iterator and return self.
     """
@@ -30518,18 +30374,18 @@ 

Source code in torrentfile\recheck.py -
509
+          
505
+506
+507
+508
+509
 510
 511
 512
 513
 514
 515
-516
-517
-518
-519
-520
def __next__(self):
+516
def __next__(self):
     """
     Provide the result of comparison.
     """
@@ -30585,7 +30441,11 @@ 

Source code in torrentfile\recheck.py -
638
+          
634
+635
+636
+637
+638
 639
 640
 641
@@ -30600,11 +30460,7 @@ 

650 651 652 -653 -654 -655 -656 -657

def advance(self) -> tuple:
+653
def advance(self) -> tuple:
     """
     Increment the number of pieces processed for the current file.
 
@@ -30668,7 +30524,11 @@ 

Source code in torrentfile\recheck.py -
577
+          
573
+574
+575
+576
+577
 578
 579
 580
@@ -30696,11 +30556,7 @@ 

602 603 604 -605 -606 -607 -608 -609

def next_file(self) -> bool:
+605
def next_file(self) -> bool:
     """
     Remove all references to  processed files and prepare for the next.
 
@@ -30795,7 +30651,11 @@ 

Source code in torrentfile\recheck.py -
611
+          
-          
+          
@@ -32385,7 +32241,7 @@ 

Parameters ---------- - kwargs : dict + **kwargs : dict Keyword arguments for torrent options. """ @@ -32797,13 +32653,13 @@

- + @@ -32874,7 +32730,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict Dictionary containing torrent file options. """ @@ -32886,7 +32742,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict dictionary of keyword args passed to superclass. """ super().__init__(**kwargs) @@ -32964,13 +32820,13 @@

- + @@ -32995,7 +32851,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict dictionary of keyword args passed to superclass. """ super().__init__(**kwargs) @@ -33151,13 +33007,13 @@

- + @@ -33266,7 +33122,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict Keyword arguments for torrent options. """ @@ -33670,13 +33526,13 @@

- + @@ -33776,7 +33632,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict Keyword arguments for torrent file options. """ @@ -33790,7 +33646,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict keywword arguments to pass to superclass. """ super().__init__(**kwargs) @@ -33899,13 +33755,13 @@

- + @@ -33937,7 +33793,7 @@

Parameters ---------- - kwargs : dict + **kwargs : dict keywword arguments to pass to superclass. """ super().__init__(**kwargs) @@ -36536,13 +36392,13 @@

- + @@ -36575,7 +36431,7 @@

Parameters ---------- - args : list + *args : list Filesystem locations for removing. """ for arg in args: diff --git a/tests/__init__.py b/tests/__init__.py index ac169230..a1147b4b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -82,7 +82,7 @@ def rmpath(*args): Parameters ---------- - args : list + *args : list Filesystem locations for removing. """ for arg in args: diff --git a/torrentfile/commands.py b/torrentfile/commands.py index 27fbd1ed..a999301f 100644 --- a/torrentfile/commands.py +++ b/torrentfile/commands.py @@ -145,7 +145,7 @@ def edit(args: list): return edit_torrent(metafile, editargs) -def recheck(args): +def recheck(args: list): """ Execute recheck CLI sub-command. @@ -172,7 +172,7 @@ def recheck(args): return result -def magnet(metafile): +def magnet(metafile: str): """ Create a magnet URI from a Bittorrent meta file. diff --git a/torrentfile/hasher.py b/torrentfile/hasher.py index 4787ec09..3090bf2f 100644 --- a/torrentfile/hasher.py +++ b/torrentfile/hasher.py @@ -470,11 +470,6 @@ def __next__(self) -> bytes: """ Calculate layer hashes for contents of file. - Parameters - ---------- - data : BytesIO - File opened in read mode. - Returns ------- bytes diff --git a/torrentfile/interactive.py b/torrentfile/interactive.py index 0187f9ee..ae88784f 100644 --- a/torrentfile/interactive.py +++ b/torrentfile/interactive.py @@ -37,7 +37,7 @@ def get_input(*args: tuple): # pragma: no cover Parameters ---------- - args : tuple + *args : tuple Arbitrary number of args to pass to next function Returns diff --git a/torrentfile/recheck.py b/torrentfile/recheck.py index 24fba6bf..8f87c8c1 100644 --- a/torrentfile/recheck.py +++ b/torrentfile/recheck.py @@ -155,7 +155,7 @@ def log_msg(self, *args, level=logging.INFO): Parameters ---------- - args : dict + *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` @@ -330,8 +330,6 @@ class FeedChecker(ProgMixin): ---------- checker : object the checker class instance. - hasher : Any - hashing class for calculating piece hashes. default=None """ def __init__(self, checker: Checker): @@ -484,8 +482,6 @@ class HashChecker(ProgMixin): ---------- checker : Object the checker instance that maintains variables. - hasher : Object - the version specific hashing class for torrent content. """ def __init__(self, checker: Checker): diff --git a/torrentfile/torrent.py b/torrentfile/torrent.py index ecf2188d..e3b0b0cf 100644 --- a/torrentfile/torrent.py +++ b/torrentfile/torrent.py @@ -410,7 +410,7 @@ class TorrentFile(MetaFile, ProgMixin): Parameters ---------- - kwargs : dict + **kwargs : dict Dictionary containing torrent file options. """ @@ -422,7 +422,7 @@ def __init__(self, **kwargs): Parameters ---------- - kwargs : dict + **kwargs : dict dictionary of keyword args passed to superclass. """ super().__init__(**kwargs) @@ -467,7 +467,7 @@ class TorrentFileV2(MetaFile, ProgMixin): Parameters ---------- - kwargs : dict + **kwargs : dict Keyword arguments for torrent file options. """ @@ -481,7 +481,7 @@ def __init__(self, **kwargs): Parameters ---------- - kwargs : dict + **kwargs : dict keywword arguments to pass to superclass. """ super().__init__(**kwargs) @@ -553,7 +553,7 @@ class TorrentFileHybrid(MetaFile, ProgMixin): Parameters ---------- - kwargs : dict + **kwargs : dict Keyword arguments for torrent options. """ @@ -649,7 +649,7 @@ class TorrentAssembler(MetaFile): Parameters ---------- - kwargs : dict + **kwargs : dict Keyword arguments for torrent options. """ From e5282a3d84aec7e97879b2d820f611486cb40a13 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 May 2022 21:28:12 -0700 Subject: [PATCH 3/7] edited readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b369b12..5965cd87 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,9 @@ or in the _`docs`_ directory. ## 🚀 Usage -![Basic Usage](https://github.com/alexpdev/torrentfile/blob/master/assets/TorrentFileBasicUsage.gif?raw=True) + +[![asciicast](https://asciinema.org/a/492737.svg)](https://asciinema.org/a/492737) + ```sh Usage From 000b867f8faafc9bb83347b7c855cf67edf2dfc4 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 May 2022 23:02:24 -0700 Subject: [PATCH 4/7] expirementing with readme embeds --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5965cd87..16874c77 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ or in the _`docs`_ directory. ## 🚀 Usage -[![asciicast](https://asciinema.org/a/492737.svg)](https://asciinema.org/a/492737) +[![asciicast](https://asciinema.org/a/MPYXYeOaC32ehWKhxOBl4ePWx.svg)](https://asciinema.org/a/MPYXYeOaC32ehWKhxOBl4ePWx?autoplay=1&loop=0) ```sh From 7d1fb608c22e5ad77d2285791a143719cf4b8a04 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 May 2022 23:04:24 -0700 Subject: [PATCH 5/7] expirementing with readme embeds 2 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 16874c77..1fb2e5e3 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,11 @@ or in the _`docs`_ directory. ## 🚀 Usage + [![asciicast](https://asciinema.org/a/MPYXYeOaC32ehWKhxOBl4ePWx.svg)](https://asciinema.org/a/MPYXYeOaC32ehWKhxOBl4ePWx?autoplay=1&loop=0) + + + ```sh From 85d88657f2c92834e17f0d6397c7019d3f964bb9 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 May 2022 15:38:10 -0700 Subject: [PATCH 6/7] final push --- .gitignore | 1 - Makefile | 4 +- README.md | 10 +- assets/TorrentFileBasicUsage.gif | Bin 120557 -> 174633 bytes assets/{favicon.ico => torrentfile-icon.ico} | Bin assets/{favicon.png => torrentfile-icon.png} | Bin assets/torrentfile.ico | Bin 67244 -> 0 bytes docs/404.html | 6 +- docs/Apache2/index.html | 10 +- docs/Commands/index.html | 16 +- docs/changelog/index.html | 10 +- docs/index.html | 12 +- docs/man/index.html | 16 +- docs/objects.inv | Bin 1959 -> 1187 bytes docs/search/search_index.json | 2 +- docs/sitemap.xml.gz | Bin 256 -> 256 bytes docs/source/index.html | 28674 +---------------- mkdocs.yml | 9 +- pyproject.toml | 2 + setup.cfg | 42 + site/index.md | 2 +- site/source.md | 20 +- tests/test_utils.py | 8 + torrentfile/__init__.py | 16 +- torrentfile/__main__.py | 8 +- torrentfile/cli.py | 2 +- torrentfile/recheck.py | 4 +- tox.ini | 2 +- 28 files changed, 457 insertions(+), 28419 deletions(-) rename assets/{favicon.ico => torrentfile-icon.ico} (100%) rename assets/{favicon.png => torrentfile-icon.png} (100%) delete mode 100644 assets/torrentfile.ico create mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore index 1a081fdf..6e6b0da4 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,6 @@ testfile # Unit test / coverage reports output.json pytest_html_report.html - htmlcov/ .tox/ .coverage diff --git a/Makefile b/Makefile index d18db1e3..4e42c371 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,8 @@ export RENAME_FILE BROWSER := python -c "$$BROWSER_PYSCRIPT" +RENAME := python -c "$$RENAME_FILE" + help: @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) @@ -87,4 +89,4 @@ release: clean test lint ## create executables for release mkdir ./dist/temp cp ./dist/torrentfile.exe ./dist/temp/ 7z a ./dist/temp.zip ./dist/temp - python -c $$RENAME_FILE + @python -c "$$RENAME_FILE" diff --git a/README.md b/README.md index 1fb2e5e3..83b03755 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TorrentFile -![torrentfile](https://github.com/alexpdev/torrentfile/blob/master/assets/torrentfile.png?raw=true) +![torrentfile](https://github.com/alexpdev/torrentfile/blob/master/site/images/torrentfile.png?raw=true) ------ @@ -54,13 +54,7 @@ or in the _`docs`_ directory. ## 🚀 Usage - - -[![asciicast](https://asciinema.org/a/MPYXYeOaC32ehWKhxOBl4ePWx.svg)](https://asciinema.org/a/MPYXYeOaC32ehWKhxOBl4ePWx?autoplay=1&loop=0) - - - - +![Basic Usage](https://github.com/alexpdev/torrentfile/blob/master/assets/TorrentFileBasicUsage.gif?raw=True) ```sh Usage diff --git a/assets/TorrentFileBasicUsage.gif b/assets/TorrentFileBasicUsage.gif index 3c2329a5a883c6c79d086315112546e41b0fdedc..5d6efbea3f780802f02542b22868a63b41614582 100644 GIT binary patch literal 174633 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 z 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

D3(J42zVJjI8&-bZZaR{|VrN!&y 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%0D8X^%fQ1RQ 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 NSxHd=4l?yk{y$iGP-FlA diff --git a/assets/favicon.ico b/assets/torrentfile-icon.ico similarity index 100% rename from assets/favicon.ico rename to assets/torrentfile-icon.ico diff --git a/assets/favicon.png b/assets/torrentfile-icon.png similarity index 100% rename from assets/favicon.png rename to assets/torrentfile-icon.png diff --git a/assets/torrentfile.ico b/assets/torrentfile.ico deleted file mode 100644 index 51c322b96e9fb754e7d29cfe8c7bd6a402004210..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67244 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 - Help Messages + Help @@ -237,7 +237,7 @@

  • - Man Page + Details
  • @@ -282,7 +282,7 @@
  • - Source Code + API
  • diff --git a/docs/Apache2/index.html b/docs/Apache2/index.html index 72e3de1c..ad13ebcc 100644 --- a/docs/Apache2/index.html +++ b/docs/Apache2/index.html @@ -229,7 +229,7 @@
  • - Help Messages + Help
  • @@ -244,7 +244,7 @@
  • - Man Page + Details
  • @@ -406,7 +406,7 @@
  • - Source Code + API
  • @@ -729,13 +729,13 @@

    APPENDIX: How to - diff --git a/docs/index.html b/docs/index.html index 04779838..e32c2eb9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -366,7 +366,7 @@
  • - Help Messages + Help
  • @@ -381,7 +381,7 @@
  • - Man Page + Details
  • @@ -426,7 +426,7 @@
  • - Source Code + API
  • @@ -580,7 +580,7 @@

    TorrentFile

    -

    torrentfile

    +

    torrentfile


    GitHub repo size GitHub License @@ -734,13 +734,13 @@

    GUI

    - diff --git a/docs/objects.inv b/docs/objects.inv index b7d866e7d87e9bcadc8501a19de668ee5d366acf..e8dbf092a82aedc137344d4a082582dac5f4ffdb 100644 GIT binary patch delta 1070 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 diff --git a/docs/search/search_index.json b/docs/search/search_index.json index b7766605..0e3740a7 100644 --- a/docs/search/search_index.json +++ b/docs/search/search_index.json @@ -1 +1 @@ -{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"TorrentFile \ud83c\udf10 Overview A simple and convenient tool for creating, reviewing, editing, and/or validating bittorrent meta files (aka torrent files). torrentfile supports all versions of Bittorrent files, including hybrid meta files, and has full unicode support. A GUI frontend for this project can be found at https://github.com/alexpdev/TorrentfileQt \ud83d\udd0c Requirements Python 3.7+ Tested on Linux, Windows and Mac \ud83d\udcbb Install via PyPi: pip install torrentfile via Git: git clone https://github.com/alexpdev/torrentfile.git cd torrentfile pip install . Download pre-compiled binaries from the release page . \ud83d\udcda Documentation Documentation can be found here or in the docs directory. torrentfile is under active development, and is subject to significant changes in the codebase between releases. \ud83d\ude80 Usage Usage ===== torrentfile [options] [command-options] ... Create, edit, show details, and check any version Bittorrent file from command line Commands -------- create (c, new) Create a torrent meta file. edit (e) Edit existing torrent meta file. magnet (m) Create magnet url from an existing Bittorrent meta file. recheck (r, check) Calculate amount of torrent meta files content is found on disk. info (i) Show detailed information about a torrent file. Usage examples can be found in the project documentation on the examples page. \ud83d\udcdd License Distributed under Apache v2 software license. See LICENSE for more information. \ud83d\udca1 Issues & Requests & PRs If you encounter any bugs or would like to request a new feature please open a new issue. PRs and other contributions are welcome https://github.com/alexpdev/torrentfile/issues Creating Torrents Basic torrent file createion > torrentfile create /path/to/content The -t --tracker -a --announce flags add one or more urls to list of trackers. > torrentfile create /path/to/content --tracker http://tracker1.com > torrentfile create -t http://tracker2 http://tracker3 --private /path/to/content > torrentfile create --tracker http://tracker /path/to/content > torrentfile create -t http://tracker1 http://tracker2 /path/to/content the --private flag indicates use by a private tracker the --source flag can be used to help with cross-seeding > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 The options for controlling the progress bar using --prog or --progress : 0 : show no progress bar at all 1 : show progress bar (default) > torrentfile -t http://tracker.com --progress 2 /path/to/content > torrentfile --prog 0 /path/to/content to specify the save location use the -o or --out flags if the path points to directory the name of torrent is autofilled. > torrentfile create -o /specific/path/name.torrent ./content to create files using bittorrent v2 use --meta-version 2 likewise --meta-version 3 creates a hybrid torrent file. > torrentfile create --meta-version 2 /path/to/content > torrentfile create --meta-version 3 /path/to/content Check/Recheck Torrent recheck torrent file /path/to/some.torrent with /path/to/content > torrentfile recheck /path/to/some.torrent /path/to/content Edit Torrent edit a torrent file > torrentfile edit [options] Create Magnet To create a magnet URI for a pre-existing torrent meta file, use the sub-command magnet or m with the path to the torrent file. > torrentfile magnet /path/to/some.torrent Interactive Mode (expiremental) Alternatively to supplying a bunch of command line arguments, interactive mode allows users to specify program options one at a time from a series of prompts. to activate interactive mode use -i or --interactive flag > torrentfile -i GUI If you prefer a windowed gui please check out the official GUI frontend here","title":"Home"},{"location":"#torrentfile","text":"","title":"TorrentFile"},{"location":"#overview","text":"A simple and convenient tool for creating, reviewing, editing, and/or validating bittorrent meta files (aka torrent files). torrentfile supports all versions of Bittorrent files, including hybrid meta files, and has full unicode support. A GUI frontend for this project can be found at https://github.com/alexpdev/TorrentfileQt","title":"\ud83c\udf10 Overview"},{"location":"#requirements","text":"Python 3.7+ Tested on Linux, Windows and Mac","title":"\ud83d\udd0c Requirements"},{"location":"#install","text":"via PyPi: pip install torrentfile via Git: git clone https://github.com/alexpdev/torrentfile.git cd torrentfile pip install . Download pre-compiled binaries from the release page .","title":"\ud83d\udcbb Install"},{"location":"#documentation","text":"Documentation can be found here or in the docs directory. torrentfile is under active development, and is subject to significant changes in the codebase between releases.","title":"\ud83d\udcda Documentation"},{"location":"#usage","text":"Usage ===== torrentfile [options] [command-options] ... Create, edit, show details, and check any version Bittorrent file from command line Commands -------- create (c, new) Create a torrent meta file. edit (e) Edit existing torrent meta file. magnet (m) Create magnet url from an existing Bittorrent meta file. recheck (r, check) Calculate amount of torrent meta files content is found on disk. info (i) Show detailed information about a torrent file. Usage examples can be found in the project documentation on the examples page.","title":"\ud83d\ude80 Usage"},{"location":"#license","text":"Distributed under Apache v2 software license. See LICENSE for more information.","title":"\ud83d\udcdd License"},{"location":"#issues-requests-prs","text":"If you encounter any bugs or would like to request a new feature please open a new issue. PRs and other contributions are welcome https://github.com/alexpdev/torrentfile/issues","title":"\ud83d\udca1 Issues & Requests & PRs"},{"location":"#creating-torrents","text":"Basic torrent file createion > torrentfile create /path/to/content The -t --tracker -a --announce flags add one or more urls to list of trackers. > torrentfile create /path/to/content --tracker http://tracker1.com > torrentfile create -t http://tracker2 http://tracker3 --private /path/to/content > torrentfile create --tracker http://tracker /path/to/content > torrentfile create -t http://tracker1 http://tracker2 /path/to/content the --private flag indicates use by a private tracker the --source flag can be used to help with cross-seeding > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 The options for controlling the progress bar using --prog or --progress : 0 : show no progress bar at all 1 : show progress bar (default) > torrentfile -t http://tracker.com --progress 2 /path/to/content > torrentfile --prog 0 /path/to/content to specify the save location use the -o or --out flags if the path points to directory the name of torrent is autofilled. > torrentfile create -o /specific/path/name.torrent ./content to create files using bittorrent v2 use --meta-version 2 likewise --meta-version 3 creates a hybrid torrent file. > torrentfile create --meta-version 2 /path/to/content > torrentfile create --meta-version 3 /path/to/content","title":"Creating Torrents"},{"location":"#checkrecheck-torrent","text":"recheck torrent file /path/to/some.torrent with /path/to/content > torrentfile recheck /path/to/some.torrent /path/to/content","title":"Check/Recheck Torrent"},{"location":"#edit-torrent","text":"edit a torrent file > torrentfile edit [options] ","title":"Edit Torrent"},{"location":"#create-magnet","text":"To create a magnet URI for a pre-existing torrent meta file, use the sub-command magnet or m with the path to the torrent file. > torrentfile magnet /path/to/some.torrent","title":"Create Magnet"},{"location":"#interactive-mode-expiremental","text":"Alternatively to supplying a bunch of command line arguments, interactive mode allows users to specify program options one at a time from a series of prompts. to activate interactive mode use -i or --interactive flag > torrentfile -i","title":"Interactive Mode (expiremental)"},{"location":"#gui","text":"If you prefer a windowed gui please check out the official GUI frontend here","title":"GUI"},{"location":"Apache2/","text":"Apache License Version 2.0, January 2004 < http://www.apache.org/licenses/ > Terms and Conditions for use, reproduction, and distribution 1. Definitions \u201cLicense\u201d shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. \u201cLicensor\u201d shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. \u201cLegal Entity\u201d shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \u201ccontrol\u201d means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. \u201cYou\u201d (or \u201cYour\u201d) shall mean an individual or Legal Entity exercising permissions granted by this License. \u201cSource\u201d form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. \u201cObject\u201d form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. \u201cWork\u201d shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). \u201cDerivative Works\u201d shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. \u201cContribution\u201d shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \u201csubmitted\u201d means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \u201cNot a Contribution.\u201d \u201cContributor\u201d shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a \u201cNOTICE\u201d text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \u201cAS IS\u201d BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets [] replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \u201cprinted page\u201d as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"License"},{"location":"Apache2/#apache-license","text":"Version 2.0, January 2004 < http://www.apache.org/licenses/ >","title":"Apache License"},{"location":"Apache2/#terms-and-conditions-for-use-reproduction-and-distribution","text":"","title":"Terms and Conditions for use, reproduction, and distribution"},{"location":"Apache2/#1-definitions","text":"\u201cLicense\u201d shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. \u201cLicensor\u201d shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. \u201cLegal Entity\u201d shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \u201ccontrol\u201d means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. \u201cYou\u201d (or \u201cYour\u201d) shall mean an individual or Legal Entity exercising permissions granted by this License. \u201cSource\u201d form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. \u201cObject\u201d form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. \u201cWork\u201d shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). \u201cDerivative Works\u201d shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. \u201cContribution\u201d shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \u201csubmitted\u201d means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \u201cNot a Contribution.\u201d \u201cContributor\u201d shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.","title":"1. Definitions"},{"location":"Apache2/#2-grant-of-copyright-license","text":"Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.","title":"2. Grant of Copyright License"},{"location":"Apache2/#3-grant-of-patent-license","text":"Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.","title":"3. Grant of Patent License"},{"location":"Apache2/#4-redistribution","text":"You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a \u201cNOTICE\u201d text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.","title":"4. Redistribution"},{"location":"Apache2/#5-submission-of-contributions","text":"Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.","title":"5. Submission of Contributions"},{"location":"Apache2/#6-trademarks","text":"This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.","title":"6. Trademarks"},{"location":"Apache2/#7-disclaimer-of-warranty","text":"Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \u201cAS IS\u201d BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.","title":"7. Disclaimer of Warranty"},{"location":"Apache2/#8-limitation-of-liability","text":"In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.","title":"8. Limitation of Liability"},{"location":"Apache2/#9-accepting-warranty-or-additional-liability","text":"While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS","title":"9. Accepting Warranty or Additional Liability"},{"location":"Apache2/#appendix-how-to-apply-the-apache-license-to-your-work","text":"To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets [] replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \u201cprinted page\u201d as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"APPENDIX: How to apply the Apache License to your work"},{"location":"Commands/","text":"TorrentFile CLI Menu Help Messages Main Usage ===== torrentfile [-h] [-i] [-V] [-v] ... CLI Tool for creating, checking, editing... Bittorrent meta files. TorrentFile supports all versions of torrent files. Options ------- -h, --help show this help message and exit -i, --interactive select program options interactively -V, --version show program version and exit -v, --verbose output debug information Actions ------- c (create, new) Create a torrent meta file. e (edit) Edit existing torrent meta file. m (magnet) Create magnet url from an existing Bittorrent meta file. r (recheck, check) Calculate amount of torrent meta file's content is found on disk. i (info) Show detailed information about a torrent file. Create Usage ===== torrentfile c [-h] [-a [ ...]] [-p] [-s ] [-m] [-c ] [-o ] [-t [ ...]] [--noprogress] [--meta-version ] [--piece-length ] [-w [ ...]] Positional Arguments -------------------- Path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit -a [ ...], --announce [ ...] Alias for -t/--tracker -p, --private Create a private torrent file -s , --source Useful for cross-seeding -m, --magnet Output Magnet Link after creation completes -c , --comment Include a comment in file metadata -o , --out Output path for created .torrent file -t [ ...], --tracker [ ...] One or more Bittorrent tracker announce url(s). --prog, --progress (0) = no progress bar displayed (1) = progress bar is displayed (default) --meta-version Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid --piece-length Number of bytes per piece. (Default: None) Acceptable inputs include {14 - 24} as exponent for 2^n, or any acceptable integer value (must be power of 2). Examples:: [--piece-length 14] [--piece-length 16777216] -w [ ...], --web-seed [ ...] One or more url(s) linking to a http server hosting the torrent contents. This is useful if the torrent tracker is ever unreachable. Example:: -w url1 url2 url3 Edit Usage ===== torrentfile e [-h] [--tracker [ ...]] [--web-seed [ ...]] [--private] [--comment ] [--source ] <*.torrent> Positional Arguments -------------------- <*.torrent> path to *.torrent file Optional Arguments ------------------ -h, --help show this help message and exit --tracker [ ...] Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). --web-seed [ ...] Replace current list of web-seed urls with one or more space seperated url(s) --private Make torrent private. --comment Replaces any existing comment with --source Replaces current source with Recheck Usage ===== torrentfile r [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> path to .torrent file. path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit Magnet Usage ===== torrentfile m [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> Path to Bittorrent meta file. Optional Arguments ------------------ -h, --help show this help message and exit","title":"Help Messages"},{"location":"Commands/#torrentfile-cli-menu","text":"","title":"TorrentFile CLI Menu"},{"location":"Commands/#help-messages","text":"","title":"Help Messages"},{"location":"Commands/#main","text":"Usage ===== torrentfile [-h] [-i] [-V] [-v] ... CLI Tool for creating, checking, editing... Bittorrent meta files. TorrentFile supports all versions of torrent files. Options ------- -h, --help show this help message and exit -i, --interactive select program options interactively -V, --version show program version and exit -v, --verbose output debug information Actions ------- c (create, new) Create a torrent meta file. e (edit) Edit existing torrent meta file. m (magnet) Create magnet url from an existing Bittorrent meta file. r (recheck, check) Calculate amount of torrent meta file's content is found on disk. i (info) Show detailed information about a torrent file.","title":"Main"},{"location":"Commands/#create","text":"Usage ===== torrentfile c [-h] [-a [ ...]] [-p] [-s ] [-m] [-c ] [-o ] [-t [ ...]] [--noprogress] [--meta-version ] [--piece-length ] [-w [ ...]] Positional Arguments -------------------- Path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit -a [ ...], --announce [ ...] Alias for -t/--tracker -p, --private Create a private torrent file -s , --source Useful for cross-seeding -m, --magnet Output Magnet Link after creation completes -c , --comment Include a comment in file metadata -o , --out Output path for created .torrent file -t [ ...], --tracker [ ...] One or more Bittorrent tracker announce url(s). --prog, --progress (0) = no progress bar displayed (1) = progress bar is displayed (default) --meta-version Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid --piece-length Number of bytes per piece. (Default: None) Acceptable inputs include {14 - 24} as exponent for 2^n, or any acceptable integer value (must be power of 2). Examples:: [--piece-length 14] [--piece-length 16777216] -w [ ...], --web-seed [ ...] One or more url(s) linking to a http server hosting the torrent contents. This is useful if the torrent tracker is ever unreachable. Example:: -w url1 url2 url3","title":"Create"},{"location":"Commands/#edit","text":"Usage ===== torrentfile e [-h] [--tracker [ ...]] [--web-seed [ ...]] [--private] [--comment ] [--source ] <*.torrent> Positional Arguments -------------------- <*.torrent> path to *.torrent file Optional Arguments ------------------ -h, --help show this help message and exit --tracker [ ...] Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). --web-seed [ ...] Replace current list of web-seed urls with one or more space seperated url(s) --private Make torrent private. --comment Replaces any existing comment with --source Replaces current source with ","title":"Edit"},{"location":"Commands/#recheck","text":"Usage ===== torrentfile r [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> path to .torrent file. path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit","title":"Recheck"},{"location":"Commands/#magnet","text":"Usage ===== torrentfile m [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> Path to Bittorrent meta file. Optional Arguments ------------------ -h, --help show this help message and exit","title":"Magnet"},{"location":"changelog/","text":"TorrentFile Version 0.7.9 complete rewrite of the recheck procedures Recheck now provides more accuracy and more details improvements to the new custom progressbar changed the cli argument for the progress bar the options are now just 0 and 1 included new unit tests for all new features marked unused functions as deprecated added a new hasher object for v2 and hybrid torrents minor bug fixes and styling changes Version 0.7.8 more updates to logging major improvements to progress bar removed tqdm as dependency implemented custom progress bar logic new cli argument controlling the progress bar support for pyben 0.3.1 added threading to recheck module added mixins module unit test updates and improvements Version 0.7.5 updates to logging facility fixed bug in created hybrid torrent files fixed cli when subcomman not chosen doc updates unit test updates and improvements Version 0.7.2 cleaned up readme and help messages removed useless print statements improved CI tooling and checking minor bug fixes Version 0.7.1 split CI integration into separate platform specific files added new cli argument --cwd which changes the default save to location to the current working directory (this will be default in future) added unit tests to cover the new argument Changed license to a the more permissive Apache 2 software license Version 0.7.0 Fixed issues with logging to file. Finished adding tests for Unicode Support Deprecated some unneccessary code Clean up documentation and README removed config files no longer in use. Version 0.6.13 Fixed bug that created a torrent file with no name. Fixed bug that would error if cli path was listed after announce urls Added full unicode support. Added Unittests for new features and bug fixes Version 0.6.11 Fixed bug that occured during recheck when file of 0 length is included. Altered Recheck algorithm to process 0 length files. Only effected meta version 2 and hybrid torrent files. Added unittests to cover the situation. Version 0.6.10 Updates to documentation Integrated Type hints in source code Updated build and CI process Version 0.6.9 The --progress flag is now --noprogress Default behavior is to show progress bar use --noprogress to not show added CLI Help format strings added custom CLI help formatter class Titled Help Message section headers Fixed a bunch of error pages created by mkdocs Version 0.6.8 Documentation for newest features CLI usage examples Improved unittests made progress bar active by default Version 0.6.7 Updates to API Version 0.6.6 bug that created faulty Bittorrent V2 meta files in some instances. back to working as it should. Version 0.6.5 Support for creating Magnet URI's Added optional progress bar for torrent creation Log File handler CLI args page in documentation verbose and logging bugs multi tracker errors bug Version 0.6.4 CLI interface add subcommands added interactive mode Re-wrote the recheck module fixed documentation and docstrings linting and testing errors Version 0.6.3 Fixed Bug that would format list of trackers incorrectly CLI Bug Fixes Version 0.6.2 Bug fixes Documentation error pages Version 0.6.0 cli commands alterations debug logging during creation process Version 0.5.2 Fixed Bug that was adding wrong fields to info dict Version 0.5.0 Slew of new unit tests Stricter linting features Alternative method of -re-check feature Bug Fixes CLI help formatting errors Version 0.4.8 Improved Algorithm performance for ReCheck. Additions to documentation. Version 0.4.7 Fixed A bug that misspelled a field when creating Hybrid torrent files. Re-Check procedure for v2 and hybrid torrent file checking. Version 0.4.6 CLI Help and Usage Messages. Expanded CLI args. Completely new CheckerClass which replaces old Checker Hooks for GUI or other 3rd party apps to hook into Checking Documentation and Unit tests for new CheckerClass Version 0.4.5 Documentation and docstrings improvements Better code formating and more detailed docstrings More unit tests. Version 0.4.2 The ReChecker feature now supports v1, v2, & hybrid .torrent file. Bug in CLI for python < 3.8 Version 0.4.1 Added tests for hybrid class Added logging features new cli flag to activate debug mode Documentation theme. Fixed Bug that allowed improper piece length values. Version 0.4.0 Fixed bugs in creating hybrid files. Bug Fix that broke cli. Version 0.3.0 Added/Improved support for hybrid meta files. Many additions to testing suit including linting and coverage tests. Version 0.2.8 Styling fixes. Bug Fixes. Prelimenary support for bittorrent hybrid meta files. Bug Fixes Version 0.2.7 major imporvements to torrentfile-GUI. minor adjustments to this package for integration. Code consolidation Bug Fixes Documentation additions Implemented CI/CD Integration Version 0.2.3 Bug Fixes Code Style and Formatting Added more unittests Version 0.2.1 Bittorrent Protocol V2 Support v2 metafile options to cli v2 metafile tests Version 0.1.7 Docstrings Improvements. Added documentation rederer. Improved readme file. formatting Version 0.1.2 Added a Command Line Interface Rough Graphical User Interface Minor Bug Fixes Improved unittest coverage Version 0.1.0 added SHA256 support Feeder class for seemless file switching Fixed the primary entrypoint function. Improved docstrings Bug fixes Version 0.0.2 Added Unittests added bencode support added hashing support Version 0.0.1 Initial concept and planning","title":"Changelog"},{"location":"changelog/#torrentfile","text":"","title":"TorrentFile"},{"location":"changelog/#version-079","text":"complete rewrite of the recheck procedures Recheck now provides more accuracy and more details improvements to the new custom progressbar changed the cli argument for the progress bar the options are now just 0 and 1 included new unit tests for all new features marked unused functions as deprecated added a new hasher object for v2 and hybrid torrents minor bug fixes and styling changes","title":"Version 0.7.9"},{"location":"changelog/#version-078","text":"more updates to logging major improvements to progress bar removed tqdm as dependency implemented custom progress bar logic new cli argument controlling the progress bar support for pyben 0.3.1 added threading to recheck module added mixins module unit test updates and improvements","title":"Version 0.7.8"},{"location":"changelog/#version-075","text":"updates to logging facility fixed bug in created hybrid torrent files fixed cli when subcomman not chosen doc updates unit test updates and improvements","title":"Version 0.7.5"},{"location":"changelog/#version-072","text":"cleaned up readme and help messages removed useless print statements improved CI tooling and checking minor bug fixes","title":"Version 0.7.2"},{"location":"changelog/#version-071","text":"split CI integration into separate platform specific files added new cli argument --cwd which changes the default save to location to the current working directory (this will be default in future) added unit tests to cover the new argument Changed license to a the more permissive Apache 2 software license","title":"Version 0.7.1"},{"location":"changelog/#version-070","text":"Fixed issues with logging to file. Finished adding tests for Unicode Support Deprecated some unneccessary code Clean up documentation and README removed config files no longer in use.","title":"Version 0.7.0"},{"location":"changelog/#version-0613","text":"Fixed bug that created a torrent file with no name. Fixed bug that would error if cli path was listed after announce urls Added full unicode support. Added Unittests for new features and bug fixes","title":"Version 0.6.13"},{"location":"changelog/#version-0611","text":"Fixed bug that occured during recheck when file of 0 length is included. Altered Recheck algorithm to process 0 length files. Only effected meta version 2 and hybrid torrent files. Added unittests to cover the situation.","title":"Version 0.6.11"},{"location":"changelog/#version-0610","text":"Updates to documentation Integrated Type hints in source code Updated build and CI process","title":"Version 0.6.10"},{"location":"changelog/#version-069","text":"The --progress flag is now --noprogress Default behavior is to show progress bar use --noprogress to not show added CLI Help format strings added custom CLI help formatter class Titled Help Message section headers Fixed a bunch of error pages created by mkdocs","title":"Version 0.6.9"},{"location":"changelog/#version-068","text":"Documentation for newest features CLI usage examples Improved unittests made progress bar active by default","title":"Version 0.6.8"},{"location":"changelog/#version-067","text":"Updates to API","title":"Version 0.6.7"},{"location":"changelog/#version-066","text":"bug that created faulty Bittorrent V2 meta files in some instances. back to working as it should.","title":"Version 0.6.6"},{"location":"changelog/#version-065","text":"Support for creating Magnet URI's Added optional progress bar for torrent creation Log File handler CLI args page in documentation verbose and logging bugs multi tracker errors bug","title":"Version 0.6.5"},{"location":"changelog/#version-064","text":"CLI interface add subcommands added interactive mode Re-wrote the recheck module fixed documentation and docstrings linting and testing errors","title":"Version 0.6.4"},{"location":"changelog/#version-063","text":"Fixed Bug that would format list of trackers incorrectly CLI Bug Fixes","title":"Version 0.6.3"},{"location":"changelog/#version-062","text":"Bug fixes Documentation error pages","title":"Version 0.6.2"},{"location":"changelog/#version-060","text":"cli commands alterations debug logging during creation process","title":"Version 0.6.0"},{"location":"changelog/#version-052","text":"Fixed Bug that was adding wrong fields to info dict","title":"Version 0.5.2"},{"location":"changelog/#version-050","text":"Slew of new unit tests Stricter linting features Alternative method of -re-check feature Bug Fixes CLI help formatting errors","title":"Version 0.5.0"},{"location":"changelog/#version-048","text":"Improved Algorithm performance for ReCheck. Additions to documentation.","title":"Version 0.4.8"},{"location":"changelog/#version-047","text":"Fixed A bug that misspelled a field when creating Hybrid torrent files. Re-Check procedure for v2 and hybrid torrent file checking.","title":"Version 0.4.7"},{"location":"changelog/#version-046","text":"CLI Help and Usage Messages. Expanded CLI args. Completely new CheckerClass which replaces old Checker Hooks for GUI or other 3rd party apps to hook into Checking Documentation and Unit tests for new CheckerClass","title":"Version 0.4.6"},{"location":"changelog/#version-045","text":"Documentation and docstrings improvements Better code formating and more detailed docstrings More unit tests.","title":"Version 0.4.5"},{"location":"changelog/#version-042","text":"The ReChecker feature now supports v1, v2, & hybrid .torrent file. Bug in CLI for python < 3.8","title":"Version 0.4.2"},{"location":"changelog/#version-041","text":"Added tests for hybrid class Added logging features new cli flag to activate debug mode Documentation theme. Fixed Bug that allowed improper piece length values.","title":"Version 0.4.1"},{"location":"changelog/#version-040","text":"Fixed bugs in creating hybrid files. Bug Fix that broke cli.","title":"Version 0.4.0"},{"location":"changelog/#version-030","text":"Added/Improved support for hybrid meta files. Many additions to testing suit including linting and coverage tests.","title":"Version 0.3.0"},{"location":"changelog/#version-028","text":"Styling fixes. Bug Fixes. Prelimenary support for bittorrent hybrid meta files. Bug Fixes","title":"Version 0.2.8"},{"location":"changelog/#version-027","text":"major imporvements to torrentfile-GUI. minor adjustments to this package for integration. Code consolidation Bug Fixes Documentation additions Implemented CI/CD Integration","title":"Version 0.2.7"},{"location":"changelog/#version-023","text":"Bug Fixes Code Style and Formatting Added more unittests","title":"Version 0.2.3"},{"location":"changelog/#version-021","text":"Bittorrent Protocol V2 Support v2 metafile options to cli v2 metafile tests","title":"Version 0.2.1"},{"location":"changelog/#version-017","text":"Docstrings Improvements. Added documentation rederer. Improved readme file. formatting","title":"Version 0.1.7"},{"location":"changelog/#version-012","text":"Added a Command Line Interface Rough Graphical User Interface Minor Bug Fixes Improved unittest coverage","title":"Version 0.1.2"},{"location":"changelog/#version-010","text":"added SHA256 support Feeder class for seemless file switching Fixed the primary entrypoint function. Improved docstrings Bug fixes","title":"Version 0.1.0"},{"location":"changelog/#version-002","text":"Added Unittests added bencode support added hashing support","title":"Version 0.0.2"},{"location":"changelog/#version-001","text":"Initial concept and planning","title":"Version 0.0.1"},{"location":"man/","text":"Name torrentfile Synopsis torrentfile [options] [options] Description torrentfile is a CLI tool for creating, editing, validating, or reviewing Bittorrent files(.torrent). It supports all current versions of the Bittorrent Protocol files as well as hybrid files. Has support for generating magnet URI's for meta files. Options -h displays all relevant command line options and subcommands. -V displays program and version. -v enables debug mode and outputs a large amount of information to the terminal. -i activates interactive mode for selecting subcommands and options. Sub-commands create alias: c , new torrentfile create [options] torrentfile c [options] The create subcommand is used for generating new torrent files. The only required argument is the path( ) to the contents file or directory. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. --piece-length Uses the number that follows as the piece-length size for the newly created torrent file. If option isn't used the program will pick the ideal size. Acceptable values include 14-29 which is interpreted as the number to raise 2 by (e.g. 14 is interpreted as 16384), or any perfect power of 2 greater than or equal to 16 KiB and less than 1 GiB. Example: --piece-length 14 or --piece-length 16384 . --meta-version Use the following number as the Bittorrent version the torrent will be used on. The default is 1. Options: 1 - Bittorrent v1, 2 - Bittorrent v2, 3 - Bittorrent v1 & v2 Examples: --meta-version 1 , --meta-version 2 , --meta-version 3 -o --out Specify the full path to the newly created torrent file. The default is to save it adjacent to the content. Example: if content is at /home/user/torrents/content the default would create /home/user/torrents/content.torrent --cwd --current Changes the default save location to the current working directory. Example: if content is at /home/user/torrents/content this option would create ./content.torrent --progress --prog Options (0, 1): No status bar will be shown if 0. Otherwise the default and 1 argument means progress bar is shown info alias: i torrentfile info torrentfile i There are no optional arguments for the info subcommand. /path/to/*.torrent The relative or absolute path to the torrent file. edit alias: e torrentfile edit [options] torrentfile e [options] Each option identifies the field to edit inside the torrent file and what the new value should be. If an option is not used then its field will be ommited in the newly created torrent file. As such if the file is marked as private and it should remain that way, the -p option should be used. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. recheck alias: r , check torrentfile recheck torrentfile r There are only two arguments for the recheck command and both are mandatory. The first is the absolute or relative to the torrent file, and the second is the absolute or relative to it's content. This will display a progress bar and at the end output what percent of the torrentfile's content it found at the path indicated. It is also permitted to use the contents parent directory as the second argument and the result will be the same.","title":"Man Page"},{"location":"man/#name","text":"torrentfile","title":"Name"},{"location":"man/#synopsis","text":"torrentfile [options] [options] ","title":"Synopsis"},{"location":"man/#description","text":"torrentfile is a CLI tool for creating, editing, validating, or reviewing Bittorrent files(.torrent). It supports all current versions of the Bittorrent Protocol files as well as hybrid files. Has support for generating magnet URI's for meta files.","title":"Description"},{"location":"man/#options","text":"-h displays all relevant command line options and subcommands. -V displays program and version. -v enables debug mode and outputs a large amount of information to the terminal. -i activates interactive mode for selecting subcommands and options.","title":"Options"},{"location":"man/#sub-commands","text":"","title":"Sub-commands"},{"location":"man/#create","text":"alias: c , new torrentfile create [options] torrentfile c [options] The create subcommand is used for generating new torrent files. The only required argument is the path( ) to the contents file or directory. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. --piece-length Uses the number that follows as the piece-length size for the newly created torrent file. If option isn't used the program will pick the ideal size. Acceptable values include 14-29 which is interpreted as the number to raise 2 by (e.g. 14 is interpreted as 16384), or any perfect power of 2 greater than or equal to 16 KiB and less than 1 GiB. Example: --piece-length 14 or --piece-length 16384 . --meta-version Use the following number as the Bittorrent version the torrent will be used on. The default is 1. Options: 1 - Bittorrent v1, 2 - Bittorrent v2, 3 - Bittorrent v1 & v2 Examples: --meta-version 1 , --meta-version 2 , --meta-version 3 -o --out Specify the full path to the newly created torrent file. The default is to save it adjacent to the content. Example: if content is at /home/user/torrents/content the default would create /home/user/torrents/content.torrent --cwd --current Changes the default save location to the current working directory. Example: if content is at /home/user/torrents/content this option would create ./content.torrent --progress --prog Options (0, 1): No status bar will be shown if 0. Otherwise the default and 1 argument means progress bar is shown","title":"create"},{"location":"man/#info","text":"alias: i torrentfile info torrentfile i There are no optional arguments for the info subcommand. /path/to/*.torrent The relative or absolute path to the torrent file.","title":"info"},{"location":"man/#edit","text":"alias: e torrentfile edit [options] torrentfile e [options] Each option identifies the field to edit inside the torrent file and what the new value should be. If an option is not used then its field will be ommited in the newly created torrent file. As such if the file is marked as private and it should remain that way, the -p option should be used. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT.","title":"edit"},{"location":"man/#recheck","text":"alias: r , check torrentfile recheck torrentfile r There are only two arguments for the recheck command and both are mandatory. The first is the absolute or relative to the torrent file, and the second is the absolute or relative to it's content. This will display a progress bar and at the end output what percent of the torrentfile's content it found at the path indicated. It is also permitted to use the contents parent directory as the second argument and the result will be the same.","title":"recheck"},{"location":"source/","text":"TorrentFile API and Source Torrent Module module torrentfile. torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. Classes MetaFile \u2014 Base Class for all TorrentFile classes. TorrentFile \u2014 Class for creating Bittorrent meta files. TorrentFileV2 \u2014 Class for creating Bittorrent meta v2 files. TorrentFileHybrid \u2014 Construct the Hybrid torrent meta file with provided parameters. TorrentAssembler \u2014 Assembler class for Bittorrent version 2 and hybrid meta files. ------ Utils Module module torrentfile. utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Classes Memo \u2014 Memoice chache object. MissingPathError \u2014 Path parameter is required to specify target content. PieceLengthValueError \u2014 Piece Length parameter must equal a perfect power of 2. Functions get_file_list ( path ) (list) \u2014 Return a sorted list of file paths contained in directory. get_piece_length ( size ) (int) \u2014 Calculate the ideal piece length for bittorrent data. humanize_bytes ( amount ) (str) \u2014 Convert integer into human readable memory sized denomination. next_power_2 ( value ) (int) \u2014 Calculate the next perfect power of 2 equal to or greater than value. normalize_piece_length ( piece_length ) (int) \u2014 Verify input piece_length is valid and convert accordingly. path_piece_length ( path ) (int) \u2014 Calculate piece length for input path and contents. path_size ( path ) (int) \u2014 Return the total size of all files in path recursively. path_stat ( path ) (list) \u2014 Calculate directory statistics. ------ Edit Module module torrentfile. edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds Functions edit_torrent ( metafile , args ) (dict) \u2014 Edit the properties and values in a torrent meta file. filter_empty ( args , meta , info ) \u2014 Remove dictionary keys with empty values. ------ Interactive Module module torrentfile. interactive Module contains the procedures used for Interactive Mode. Classes InteractiveEditor \u2014 Interactive dialog class for torrent editing. InteractiveCreator \u2014 Class namespace for interactive program options. Functions create_torrent ( ) \u2014 Create new torrent file interactively. edit_action ( ) \u2014 Edit the editable values of the torrent meta file. get_input ( *args ) (str) \u2014 Determine appropriate input function to call. recheck_torrent ( ) \u2014 Check torrent download completed percentage. select_action ( ) \u2014 Operate TorrentFile program interactively through terminal. showcenter ( txt ) \u2014 Print text to screen in the center position of the terminal. showtext ( txt ) \u2014 Print contents of txt to screen. ------ CLI Module module torrentfile. cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. Classes TorrentFileHelpFormatter \u2014 Formatting class for help tips provided by the CLI. Functions activate_logger ( ) \u2014 Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. main ( ) \u2014 Initiate main function for CLI script. ------ Recheck Module module torrentfile. recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Classes Checker \u2014 Check a given file or directory to see if it matches a torrentfile. FeedChecker \u2014 Validates torrent content. HashChecker \u2014 Verify that root hashes of content files match the .torrent files. ------ Hasher Module module torrentfile. hasher Piece/File Hashers for Bittorrent meta file contents. Classes Hasher \u2014 Piece hasher for Bittorrent V1 files. HasherV2 \u2014 Calculate the root hash and piece layers for file contents. HasherHybrid \u2014 Calculate root and piece hashes for creating hybrid torrent file. FileHasher \u2014 Calculate root and piece hashes for creating hybrid torrent file. Functions merkle_root ( blocks ) (bytes) \u2014 Calculate the merkle root for a seq of sha256 hash digests. ------ torrentfile Torrentfile can create Bittorrent metafiles for any content. Both Bittorrent v1 and v2 are fully supported. Also included is a torrent torrent file checker, which can verify a .torrent file is formated correctly as well as validate files and folders against metadata. Modules: metafile: Creation/Validation of v1 .torrent files. metafile2: Creation/Validation of v2 .torrent files. torrentfile: torrentfiles Command Line Interface implementation. exceptions: Custom Exceptions used in package. utils: Utilities used throughout package. create ( args ) Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args info ( args ) Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output magnet ( metafile ) Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile : str ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri __main__ Enable calling the package directly with python from the command line. main () Start the entry point script. Source code in torrentfile\\__main__.py 26 27 28 29 30 def main (): \"\"\" Start the entry point script. \"\"\" execute () cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. TorrentFileHelpFormatter Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts __init__ ( prog , width = 40 , max_help_positions = 30 ) Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) _format_text ( text ) Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" _join_parts ( part_strings ) Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) _split_lines ( text , _ ) Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] format_headers ( parts ) staticmethod Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts activate_logger () Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" ) execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args main () Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute () commands The commands module contains the Action Commands executed by the CLI script. Each function pertains to a command line action/subcommand and drives specific features of the application. Functions create_command info_command edit_command recheck_command magnet_command create ( args ) Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args edit ( args ) Execute the edit CLI sub-command with provided arguments. Parameters: Name Type Description Default args Namespace positional and optional CLI arguments. required Returns: Type Description str path to edited torrent file. Source code in torrentfile\\commands.py 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 def edit ( args : list ): \"\"\" Execute the edit CLI sub-command with provided arguments. Parameters ---------- args : Namespace positional and optional CLI arguments. Returns ------- str path to edited torrent file. \"\"\" metafile = args . metafile logger . info ( \"Editing %s Meta File\" , str ( args . metafile )) editargs = { \"url-list\" : args . url_list , \"httpseeds\" : args . httpseeds , \"announce\" : args . announce , \"source\" : args . source , \"private\" : args . private , \"comment\" : args . comment , } return edit_torrent ( metafile , editargs ) info ( args ) Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output magnet ( metafile ) Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile : str ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri recheck ( args ) Execute recheck CLI sub-command. Parameters: Name Type Description Default args Namespace positional and optional arguments. required Returns: Type Description str The percentage of content currently saved to disk. Source code in torrentfile\\commands.py 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 def recheck ( args : list ): \"\"\" Execute recheck CLI sub-command. Parameters ---------- args : Namespace positional and optional arguments. Returns ------- str The percentage of content currently saved to disk. \"\"\" metafile = args . metafile content = args . content logger . debug ( \"Validating %s against %s contents\" , metafile , content ) checker = Checker ( metafile , content ) logger . debug ( \"Completed initialization of the Checker class\" ) result = checker . results () sys . stdout . write ( str ( result ) + \"% Match \\n \" ) sys . stdout . flush () return result edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds edit_torrent ( metafile , args ) Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta filter_empty ( args , meta , info ) Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val ) hasher Piece/File Hashers for Bittorrent meta file contents. FileHasher Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () __init__ ( path , piece_length , progress = True , hybrid = False ) Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) __iter__ () Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self __next__ () Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash _calculate_root () Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] Hasher Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec __init__ ( paths , piece_length , progress = True ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) __iter__ () Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self __next__ () Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec _handle_partial ( arr ) Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec next_file () Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False HasherHybrid Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) _calculate_root () Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] process_file ( data ) Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () HasherV2 Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) _calculate_root () Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) process_file ( fd ) Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () merkle_root ( blocks ) Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks interactive Module contains the procedures used for Interactive Mode. InteractiveCreator Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () __init__ () Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () get_props () Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () InteractiveEditor Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) __init__ ( metafile ) Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } edit_props () Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) sanatize_response ( key , response ) Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val show_current () Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) _get_input ( txt ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value _get_input_loop ( txt , func ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" ) create_torrent () Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator edit_action () Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props () get_input ( * args ) Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- *args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args ) recheck_torrent () Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results select_action () Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover showcenter ( txt ) Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string ) showtext ( txt ) Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt ) mixins Collection of classes that can be used as Mixins with other base classes. Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the progress bar mixin. And any class is eligible to use the callback mixin. CbMixin Mixin class to set a callback during hashing procedure. Source code in torrentfile\\mixins.py 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class CbMixin : \"\"\" Mixin class to set a callback during hashing procedure. \"\"\" _cb = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover set_callback ( func ) classmethod Assign a callback to the Hashing class. Parameters: Name Type Description Default func function the callback function required Source code in torrentfile\\mixins.py 40 41 42 43 44 45 46 47 48 49 50 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover ProgMixin Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods prog_start prog_update prog_close Source code in torrentfile\\mixins.py 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 class ProgMixin : \"\"\" Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods ------- prog_start prog_update prog_close \"\"\" def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start ) def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush () def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False is_active () Test to see if there is an active progress bar for object. Returns: Name Type Description bool True if there is, otherwise False. Source code in torrentfile\\mixins.py 199 200 201 202 203 204 205 206 207 208 209 210 def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False prog_close () Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. Source code in torrentfile\\mixins.py 187 188 189 190 191 192 193 194 195 196 197 def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog prog_start ( total , path , length = 50 , unit = None ) Generate a new progress bar for the given file path. Parameters: Name Type Description Default total int the total amount of units accumulating towards. required path str path to file being hashed. required length int the number of characters of the actual progress bar. 50 unit str the text representation of the value being measured. None Source code in torrentfile\\mixins.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start ) prog_update ( val ) Update progress bar with given amount of progress. Parameters: Name Type Description Default val int the number of bytes count the progress bar should increase. required Source code in torrentfile\\mixins.py 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush () ProgressBar Holds the state and details of the terminal progress bars. Parameters: Name Type Description Default total int the total amount to be accumulated. required title str the subject of the progress tracker required length int the width of the progress bar required unit str the text representation incremented required Source code in torrentfile\\mixins.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 class ProgressBar : \"\"\" Holds the state and details of the terminal progress bars. Parameters ---------- total : int the total amount to be accumulated. title : str the subject of the progress tracker length : int the width of the progress bar unit : str the text representation incremented \"\"\" def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ]) def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar ) __init__ ( total , title , length , unit , start ) Construct the progress bar object and store state of it's properties. Source code in torrentfile\\mixins.py 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ]) increment ( value ) Increase the state of the progress bar value. Parameters: Name Type Description Default value int the amount to increment the state by. required Source code in torrentfile\\mixins.py 96 97 98 99 100 101 102 103 104 105 def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value pbar () Return the size of the filled portion of the progress bar. Returns: Name Type Description str the progress bar characters Source code in torrentfile\\mixins.py 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar ) waiting ( msg , flag , timeout = 180 ) Show loading message while thread completes processing. Parameters: Name Type Description Default msg str Message string printed before the progress bar required flag list Once flag is filled exit loop required timeout int max amount of time to run the function. 180 Source code in torrentfile\\mixins.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 def waiting ( msg , flag , timeout = 180 ): \"\"\" Show loading message while thread completes processing. Parameters ---------- msg : str Message string printed before the progress bar flag : list Once flag is filled exit loop timeout : int max amount of time to run the function. \"\"\" then = time . time () codes , fill = list ( range ( 9617 , 9620 )), chr ( 9619 ) size = idx = 0 total = shutil . get_terminal_size () . columns - len ( msg ) - 20 def output ( text ): \"\"\" Print parameter message to the console. Parameters ---------- text : str output message \"\"\" sys . stdout . write ( text ) sys . stdout . flush () output ( \" \\n \" ) while not flag : time . sleep ( 0.16 ) filled = ( fill * size ) + chr ( codes [ idx ]) + ( \" \" * ( total - size )) output ( f \" { msg } : { filled } \\r \" ) idx = idx + 1 if idx + 1 < len ( codes ) else 0 size = size + 1 if size < total else 0 if time . time () - then > timeout : break output ( \" \\n \" ) recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Checker Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required Example >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 __init__ ( metafile , path ) Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () check_paths () Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) find_root ( path ) Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) iter_hashes () Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 log_msg ( * args , level = logging . INFO ) Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) piece_checker () Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker register_callback ( hook ) classmethod Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook results () Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result walk_file_tree ( tree , partials ) Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) FeedChecker Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial __init__ ( checker ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 335 336 337 338 339 340 341 342 343 344 345 346 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 348 349 350 351 352 353 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self __next__ () Yield back result of comparison. Source code in torrentfile\\recheck.py 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) _gen_padding ( partial , length , read = 0 ) Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial extract ( path , partial ) Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad iter_pieces () Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () HashChecker Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required Source code in torrentfile\\recheck.py 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size Padder Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( length , piece_length ) Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 530 531 532 533 534 535 536 537 538 539 540 541 542 543 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () __iter__ () Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 545 546 547 548 549 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover __next__ () Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( checker ) Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 487 488 489 490 491 492 493 494 495 496 497 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 499 500 501 502 503 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self __next__ () Provide the result of comparison. Source code in torrentfile\\recheck.py 505 506 507 508 509 510 511 512 513 514 515 516 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr advance () Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size next_file () Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False process_current () Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. MetaFile Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta __init__ ( path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ ) Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name assemble () Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError set_callback ( func ) classmethod Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) sort_meta () Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta write ( outfile = None ) Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta TorrentAssembler Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFile Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- **kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces __init__ ( ** kwargs ) Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () assemble () Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces TorrentFileHybrid Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFileV2 Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree __init__ ( ** kwargs ) Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree assemble () Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Memo Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __call__ ( path ) Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __init__ ( func ) Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} MissingPathError Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) PieceLengthValueError Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) _filelist_total ( path ) Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist ) filelist_total ( pathstring ) Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError get_file_list ( path ) Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist get_piece_length ( size ) Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp humanize_bytes ( amount ) Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\" next_power_2 ( value ) Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start normalize_piece_length ( piece_length ) Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError path_piece_length ( path ) Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize ) path_size ( path ) Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size path_stat ( path ) Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length ) version Holds the release version number. torrentfile.cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. TorrentFileHelpFormatter Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts __init__ ( prog , width = 40 , max_help_positions = 30 ) Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) _format_text ( text ) Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" _join_parts ( part_strings ) Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) _split_lines ( text , _ ) Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] format_headers ( parts ) staticmethod Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts activate_logger () Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" ) execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args main () Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute () torrentfile.edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds edit_torrent ( metafile , args ) Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta filter_empty ( args , meta , info ) Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val ) torrentfile.hasher Piece/File Hashers for Bittorrent meta file contents. FileHasher Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () __init__ ( path , piece_length , progress = True , hybrid = False ) Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) __iter__ () Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self __next__ () Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash _calculate_root () Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close () _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] Hasher Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec __init__ ( paths , piece_length , progress = True ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) __iter__ () Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self __next__ () Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec _handle_partial ( arr ) Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec next_file () Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False HasherHybrid Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) _calculate_root () Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] process_file ( data ) Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () HasherV2 Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) __init__ ( path , piece_length , progress = True ) Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) _calculate_root () Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes ) process_file ( fd ) Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () merkle_root ( blocks ) Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks torrentfile.interactive Module contains the procedures used for Interactive Mode. InteractiveCreator Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () __init__ () Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () get_props () Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write () InteractiveEditor Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) __init__ ( metafile ) Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } edit_props () Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args ) sanatize_response ( key , response ) Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val show_current () Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) _get_input ( txt ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value _get_input_loop ( txt , func ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" ) create_torrent () Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator edit_action () Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props () get_input ( * args ) Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- *args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args ) recheck_torrent () Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results select_action () Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover showcenter ( txt ) Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string ) showtext ( txt ) Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt ) torrentfile.recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Checker Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required Example >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 __init__ ( metafile , path ) Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () check_paths () Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) find_root ( path ) Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) iter_hashes () Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0 log_msg ( * args , level = logging . INFO ) Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) piece_checker () Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker register_callback ( hook ) classmethod Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook results () Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result walk_file_tree ( tree , partials ) Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) FeedChecker Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial __init__ ( checker ) Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 335 336 337 338 339 340 341 342 343 344 345 346 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 348 349 350 351 352 353 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self __next__ () Yield back result of comparison. Source code in torrentfile\\recheck.py 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) _gen_padding ( partial , length , read = 0 ) Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial extract ( path , partial ) Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad iter_pieces () Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () HashChecker Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required Source code in torrentfile\\recheck.py 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size Padder Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( length , piece_length ) Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 530 531 532 533 534 535 536 537 538 539 540 541 542 543 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () __iter__ () Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 545 546 547 548 549 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover __next__ () Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration __init__ ( checker ) Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 487 488 489 490 491 492 493 494 495 496 497 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 __iter__ () Assign iterator and return self. Source code in torrentfile\\recheck.py 499 500 501 502 503 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self __next__ () Provide the result of comparison. Source code in torrentfile\\recheck.py 505 506 507 508 509 510 511 512 513 514 515 516 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr advance () Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size next_file () Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False process_current () Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err torrentfile.torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. MetaFile Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta __init__ ( path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ ) Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name assemble () Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError set_callback ( func ) classmethod Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) sort_meta () Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta write ( outfile = None ) Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta TorrentAssembler Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFile Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- **kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces __init__ ( ** kwargs ) Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () assemble () Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces TorrentFileHybrid Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree assemble () Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info TorrentFileV2 Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree __init__ ( ** kwargs ) Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () _traverse ( path ) Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree assemble () Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers torrentfile.utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Memo Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __call__ ( path ) Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result __init__ ( func ) Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} MissingPathError Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message ) PieceLengthValueError Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) __init__ ( message = None ) Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message ) _filelist_total ( path ) Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist ) filelist_total ( pathstring ) Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError get_file_list ( path ) Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist get_piece_length ( size ) Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp humanize_bytes ( amount ) Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\" next_power_2 ( value ) Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start normalize_piece_length ( piece_length ) Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError path_piece_length ( path ) Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize ) path_size ( path ) Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size path_stat ( path ) Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length ) torrentfile.version Holds the release version number. tests Unittest package init module. dir1 () Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 168 169 170 171 172 173 174 175 176 177 178 179 @pytest . fixture ( scope = \"package\" ) def dir1 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir () yield root rmpath ( root ) dir2 () Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 182 183 184 185 186 187 188 189 190 191 192 193 @pytest . fixture () def dir2 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir ( ext = \"2\" ) yield Path ( root ) rmpath ( root ) file1 () Return the path to a temporary file package scope. Source code in tests\\__init__.py 241 242 243 244 245 246 247 248 @pytest . fixture ( scope = \"package\" ) def file1 (): \"\"\" Return the path to a temporary file package scope. \"\"\" path = tempfile () yield path rmpath ( path ) file2 () Return the path to a temporary file no scope. Source code in tests\\__init__.py 297 298 299 300 301 302 303 304 @pytest . fixture () def file2 (): \"\"\" Return the path to a temporary file no scope. \"\"\" path = tempfile () yield path rmpath ( path ) filemeta1 ( file1 , request ) Test fixture for generating metafile for all versions of torrents. Source code in tests\\__init__.py 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 @pytest . fixture ( scope = \"package\" , params = torrents ()) def filemeta1 ( file1 , request ): \"\"\" Test fixture for generating metafile for all versions of torrents. \"\"\" args = { \"path\" : file1 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file1 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile ) filemeta2 ( file2 , request ) Test fixture for generating a meta file no scope. Source code in tests\\__init__.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 @pytest . fixture ( params = torrents ()) def filemeta2 ( file2 , request ): \"\"\" Test fixture for generating a meta file no scope. \"\"\" args = { \"path\" : file2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url7\" , \"url8\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file2 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile ) metafile1 ( dir1 , request ) Create a standard metafile for testing. Source code in tests\\__init__.py 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 @pytest . fixture ( scope = \"package\" , params = torrents ()) def metafile1 ( dir1 , request ): \"\"\" Create a standard metafile for testing. \"\"\" versions = torrents () args = { \"path\" : dir1 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir1 ) + str ( versions . index ( torrent_class )) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile ) metafile2 ( dir2 , request ) Create a standard metafile for testing. Source code in tests\\__init__.py 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 @pytest . fixture ( params = torrents ()) def metafile2 ( dir2 , request ): \"\"\" Create a standard metafile for testing. \"\"\" args = { \"path\" : dir2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"httpseeds\" : [ \"url6\" , \"url7\" ], \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir2 ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile ) rmpath ( * args ) Remove file or directory path. Parameters: Name Type Description Default *args list Filesystem locations for removing. () Source code in tests\\__init__.py 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def rmpath ( * args ): \"\"\"Remove file or directory path. Parameters ---------- *args : list Filesystem locations for removing. \"\"\" for arg in args : if not os . path . exists ( arg ): continue if os . path . isdir ( arg ): try : shutil . rmtree ( arg ) except PermissionError : # pragma: nocover pass elif os . path . isfile ( arg ): try : os . remove ( arg ) except PermissionError : # pragma: nocover pass sizedfiles ( dir2 , sizes , request ) Generate variable sized meta files for testing, no scope. Source code in tests\\__init__.py 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 @pytest . fixture ( params = torrents ()) def sizedfiles ( dir2 , sizes , request ): \"\"\" Generate variable sized meta files for testing, no scope. \"\"\" versions = torrents () args = { \"content\" : dir2 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , \"piece_length\" : sizes , } torrent_class = request . param version = str ( versions . index ( torrent_class )) outfile = str ( dir2 ) + version + str ( sizes ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile ) sizes ( request ) Generate powers of 2 for file creation package scope. Source code in tests\\__init__.py 159 160 161 162 163 164 165 @pytest . fixture ( scope = \"package\" , params = [ 2 ** i for i in range ( 15 , 20 )]) def sizes ( request ): \"\"\" Generate powers of 2 for file creation package scope. \"\"\" size = request . param yield size teardown () Remove all temporary directories and files. Source code in tests\\__init__.py 141 142 143 144 145 146 147 148 149 @atexit . register def teardown (): # pragma: nocover \"\"\" Remove all temporary directories and files. \"\"\" root = Path ( __file__ ) . parent / \"TESTDIR\" for path in [ root , \"./torrentfile.log\" ]: if os . path . exists ( path ): rmpath ( path ) tempdir ( ext = '1' ) Create temporary directory. Parameters: Name Type Description Default ext str , optional extension to file names, by default \"1\" '1' Returns: Type Description str path to common root for directory. Source code in tests\\__init__.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 def tempdir ( ext = \"1\" ): \"\"\"Create temporary directory. Parameters ---------- ext : str, optional extension to file names, by default \"1\" Returns ------- str path to common root for directory. \"\"\" layouts = { \"1\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.mp4\" , f \"dir { ext } /file3.mp3\" , f \"dir { ext } /file4.zip\" , f \"dir { ext } /file5.txt\" , f \"dir { ext } /subdir1/subdir2/file.7z\" , f \"dir { ext } /subdir/subdir/file4.rar\" , f \"dir { ext } /subdir/subdir/file4.r01\" , ], \"2\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.jpg\" , f \"dir { ext } /subdir/file2.mp4\" , f \"dir { ext } /subdir/file3.mp3\" , ], } paths = [] for path in layouts [ ext ]: temps = tempfile ( path = path , exp = 18 ) paths . append ( temps ) return os . path . commonpath ( paths ) tempfile ( path = None , exp = 18 ) Create temporary file. Creates a temporary file for unittesting purposes.py Parameters: Name Type Description Default path str , optional relative path to temporary files, by default None None exp int , optional Exponent used to determine size of file., by default 18 18 Returns: Type Description str absolute path to file. Source code in tests\\__init__.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 def tempfile ( path = None , exp = 18 ): \"\"\"Create temporary file. Creates a temporary file for unittesting purposes.py Parameters ---------- path : str, optional relative path to temporary files, by default None exp : int, optional Exponent used to determine size of file., by default 18 Returns ------- str absolute path to file. \"\"\" seq = ( string . printable + string . whitespace ) . encode ( \"utf-8\" ) root = Path ( __file__ ) . parent / \"TESTDIR\" if not os . path . exists ( root ): os . mkdir ( root ) if not path : path = root / ( str ( datetime . timestamp ( datetime . now ())) + \".file\" ) parts = Path ( path ) . parts partial = root for i , part in enumerate ( parts ): partial = partial / part if i == len ( parts ) - 1 : with open ( partial , \"wb\" ) as binfile : size = 2 ** exp while size > 0 : if len ( seq ) < size : binfile . write ( seq ) size -= len ( seq ) seq += seq else : binfile . write ( seq [: size ]) size -= size else : if not os . path . exists ( partial ): os . mkdir ( partial ) return partial torrents () Return seq of torrentfile objects. Source code in tests\\__init__.py 152 153 154 155 156 def torrents (): \"\"\" Return seq of torrentfile objects. \"\"\" return [ TorrentFile , TorrentFileV2 , TorrentFileHybrid , TorrentAssembler ] test_cli Testing functions for the command line interface. folder ( dir1 ) Yield a folder object as fixture. Source code in tests\\test_cli.py 41 42 43 44 45 46 47 48 49 @pytest . fixture ( scope = \"module\" ) def folder ( dir1 ): \"\"\" Yield a folder object as fixture. \"\"\" sfolder = str ( dir1 ) torrent = sfolder + \".torrent\" yield ( sfolder , torrent ) rmpath ( torrent ) test_cli_announce ( folder , piece_length , version ) Test announce cli flag. Source code in tests\\test_cli.py 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce ( folder , piece_length , version ): \"\"\" Test announce cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--tracker\" , \"https://announce.org/tracker\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"announce\" ] == \"https://announce.org/tracker\" test_cli_announce_list ( folder , version ) Test announce-list cli flag. Source code in tests\\test_cli.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce_list ( folder , version ): \"\"\" Test announce-list cli flag. \"\"\" folder , torrent = folder trackers = [ \"https://announce.org/tracker\" , \"https://announce.net/tracker\" , \"https://tracker.net/announce\" , ] args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , version , \"--tracker\" , ] + trackers sys . argv = args execute () meta = pyben . load ( torrent ) for url in trackers : assert url in [ j for i in meta [ \"announce-list\" ] for j in i ] test_cli_announce_path ( dir1 , flag ) Test CLI when path is placed after the trackers flag. Source code in tests\\test_cli.py 451 452 453 454 455 456 457 458 459 460 461 462 463 @pytest . mark . parametrize ( \"flag\" , [ \"-t\" , \"-w\" , \"--announce\" , \"--web-seed\" , \"--http-seed\" ] ) def test_cli_announce_path ( dir1 , flag ): \"\"\" Test CLI when path is placed after the trackers flag. \"\"\" args = [ \"torrentfile\" , \"create\" , flag , \"https://announce1.org\" , str ( dir1 )] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_comment ( folder , piece_length , version ) Test comment cli flag. Source code in tests\\test_cli.py 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_comment ( folder , piece_length , version ): \"\"\" Test comment cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--magnet\" , \"--comment\" , \"this is a comment\" , \"--progress\" , \"1\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"comment\" ] == \"this is a comment\" test_cli_created_by ( folder , piece_length , version ) Test if created torrents recieve a created by field in meta info. Source code in tests\\test_cli.py 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_created_by ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a created by field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"TorrentFile\" in meta [ \"created by\" ] test_cli_creation_date ( folder , piece_length , version ) Test if torrents created get an accurate timestamp. Source code in tests\\test_cli.py 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_creation_date ( folder , piece_length , version ): \"\"\" Test if torrents created get an accurate timestamp. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) num = float ( meta [ \"creation date\" ]) date = datetime . datetime . fromtimestamp ( num ) now = datetime . datetime . now () assert date . day == now . day assert date . year == now . year assert date . month == now . month test_cli_cwd ( folder ) Test outfile cli flag. Source code in tests\\test_cli.py 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 def test_cli_cwd ( folder ): \"\"\" Test outfile cli flag. \"\"\" folder , _ = folder args = [ \"torrentfile\" , \"create\" , \"--cwd\" , folder , ] sys . argv = args current = os . getcwd () name = os . path . basename ( folder ) outfile = os . path . join ( current , name ) + \".torrent\" execute () assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_empty_files ( dir2 , version , progress ) Test creating torrent with empty files. Source code in tests\\test_cli.py 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"progress\" , [ \"0\" , \"1\" ]) def test_cli_empty_files ( dir2 , version , progress ): \"\"\" Test creating torrent with empty files. \"\"\" args = [ \"torrentfile\" , \"create\" , str ( dir2 ), \"--meta-version\" , version , \"--source\" , \"somesource\" , \"--prog\" , progress , ] sys . argv = args def walk ( root , count ): \"\"\" Traverse directory to edit files. \"\"\" if root . is_file (): with open ( root , \"wb\" ) as _ : return 1 elif root . is_dir (): for item in root . iterdir (): if count >= 2 : break count += walk ( item , count ) return count walk ( dir2 , 0 ) execute () outfile = str ( dir2 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_help () Test showing help notice cli flag. Source code in tests\\test_cli.py 352 353 354 355 356 357 358 359 360 361 def test_cli_help (): \"\"\" Test showing help notice cli flag. \"\"\" args = [ \"-h\" ] sys . argv = args try : assert execute () except SystemExit : assert True test_cli_outfile ( dir1 , piece_length , version ) Test outfile cli flag. Source code in tests\\test_cli.py 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_outfile ( dir1 , piece_length , version ): \"\"\" Test outfile cli flag. \"\"\" outfile = dir1 + \"test.torrent\" args = [ \"torrentfile\" , \"create\" , dir1 , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-o\" , outfile , \"--prog\" , \"1\" , ] sys . argv = args execute () assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_piece_length ( folder , piece_length , version ) Test piece length cli flag. Source code in tests\\test_cli.py 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_piece_length ( folder , piece_length , version ): \"\"\" Test piece length cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--progress\" , \"0\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"piece length\" ] == piece_length test_cli_private ( folder ) Test private cli flag. Source code in tests\\test_cli.py 85 86 87 88 89 90 91 92 93 94 def test_cli_private ( folder ): \"\"\" Test private cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--private\" ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"private\" in meta [ \"info\" ] test_cli_slash_outpath ( dir1 , sep ) Test if output when outpath ends with a /. Source code in tests\\test_cli.py 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 @pytest . mark . parametrize ( \"sep\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_outpath ( dir1 , sep ): \"\"\" Test if output when outpath ends with a /. \"\"\" if sys . platform != \"win32\" : sep = \"/\" # pragma: nocover parent = os . path . dirname ( dir1 ) + sep args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , \"-o\" , parent , str ( dir1 ), ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_slash_path ( dir1 , ending ) Test if output when path ends with a /. Source code in tests\\test_cli.py 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 @pytest . mark . parametrize ( \"ending\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_path ( dir1 , ending ): \"\"\" Test if output when path ends with a /. \"\"\" if sys . platform != \"win32\" and ending == \" \\\\ \" : # pragma: nocover ending = \"/\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , str ( dir1 ) + ending , ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile ) test_cli_v1 ( folder ) Basic create torrent cli command. Source code in tests\\test_cli.py 52 53 54 55 56 57 58 59 60 def test_cli_v1 ( folder ): \"\"\" Basic create torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_v2 ( folder ) Create torrent v2 cli command. Source code in tests\\test_cli.py 63 64 65 66 67 68 69 70 71 def test_cli_v2 ( folder ): \"\"\" Create torrent v2 cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"2\" ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_v3 ( folder ) Create hybrid torrent cli command. Source code in tests\\test_cli.py 74 75 76 77 78 79 80 81 82 def test_cli_v3 ( folder ): \"\"\" Create hybrid torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"3\" ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_web_seeds ( folder , piece_length , version ) Test if created torrents recieve a web seeds field in meta info. Source code in tests\\test_cli.py 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_web_seeds ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a web seeds field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-w\" , \"https://webseed.url/1\" , \"https://webseed.url/2\" , \"https://webseed.url/3\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"https://webseed.url/1\" in meta [ \"url-list\" ] test_cli_with_debug ( folder , piece_length , version ) Test debug mode cli flag. Source code in tests\\test_cli.py 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_debug ( folder , piece_length , version ): \"\"\" Test debug mode cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () assert os . path . exists ( torrent ) test_cli_with_source ( folder , piece_length , version ) Test source cli flag. Source code in tests\\test_cli.py 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_source ( folder , piece_length , version ): \"\"\" Test source cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--source\" , \"somesource\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"source\" ] == \"somesource\" test_fix () Test dir1 fixture is not None. Source code in tests\\test_cli.py 34 35 36 37 38 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and dir2 test_commands Testing functions for the sub-action commands from command line args. test_create_unicode_name ( file1 ) Test Unicode information in CLI args. Source code in tests\\test_commands.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 def test_create_unicode_name ( file1 ): \"\"\" Test Unicode information in CLI args. \"\"\" parent = os . path . dirname ( file1 ) filename = os . path . join ( parent , \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" ) args = [ \"torrentfile\" , \"-v\" , \"create\" , \"-a\" , \"tracker_url.com/announce_3456\" , \"tracker_url.net/announce_3456\" , \"--source\" , \"sourcetext\" , \"--comment\" , \"filename is \u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"-o\" , str ( filename ), str ( file1 ), ] sys . argv = args execute () assert os . path . exists ( filename ) test_fix () Test dir1 fixture is not None. Source code in tests\\test_commands.py 37 38 39 40 41 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and metafile1 and file1 and metafile2 and dir2 test_info ( field , file1 ) Test the info_command action from the Command Line Interface. Source code in tests\\test_commands.py 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 @pytest . mark . parametrize ( \"field\" , [ \"name\" , \"announce\" , \"source\" , \"comment\" , \"private\" , \"announce-list\" ], ) def test_info ( field , file1 ): \"\"\" Test the info_command action from the Command Line Interface. \"\"\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"url1\" , \"url2\" , \"url3\" , \"--web-seed\" , \"url4\" , \"url5\" , \"--http-seed\" , \"url6\" , \"url7\" , \"--private\" , \"--comment\" , \"ExampleComment\" , \"--source\" , \"examplesource\" , str ( file1 ), ] sys . argv = args execute () class Space : \"\"\" Stand in substitution for argparse.Namespace object. \"\"\" metafile = str ( file1 ) + \".torrent\" output = info ( Space ) assert field in output test_magnet ( metafile1 ) Test create magnet function scheme. Source code in tests\\test_commands.py 65 66 67 68 69 70 def test_magnet ( metafile1 ): \"\"\" Test create magnet function scheme. \"\"\" magnet_link = magnet ( metafile1 ) assert magnet_link . startswith ( \"magnet\" ) test_magnet_cli ( metafile1 ) Test magnet creation through CLI interface. Source code in tests\\test_commands.py 136 137 138 139 140 141 142 def test_magnet_cli ( metafile1 ): \"\"\" Test magnet creation through CLI interface. \"\"\" sys . argv [ 1 :] = [ \"m\" , str ( metafile1 )] uri = execute () assert \"magnet\" in uri test_magnet_empty () Test create magnet function scheme. Source code in tests\\test_commands.py 84 85 86 87 88 89 90 91 def test_magnet_empty (): \"\"\" Test create magnet function scheme. \"\"\" try : magnet ( \"file_that_does_not_exist\" ) except FileNotFoundError : assert True test_magnet_hex ( metafile1 ) Test create magnet function digest. Source code in tests\\test_commands.py 54 55 56 57 58 59 60 61 62 def test_magnet_hex ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) info = meta [ \"info\" ] binfo = sha1 ( pyben . dumps ( info )) . hexdigest () . upper () assert binfo in magnet_link test_magnet_no_announce_list ( metafile2 ) Test create magnet function scheme. Source code in tests\\test_commands.py 73 74 75 76 77 78 79 80 81 def test_magnet_no_announce_list ( metafile2 ): \"\"\" Test create magnet function scheme. \"\"\" meta = pyben . load ( metafile2 ) del meta [ \"announce-list\" ] pyben . dump ( meta , metafile2 ) magnet_link = magnet ( metafile2 ) assert magnet_link . startswith ( \"magnet\" ) test_magnet_uri ( metafile1 ) Test create magnet function digest. Source code in tests\\test_commands.py 44 45 46 47 48 49 50 51 def test_magnet_uri ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) announce = meta [ \"announce\" ] assert quote_plus ( announce ) in magnet_link test_merkle_root_no_blocks ( blocks ) Test running merkle root function with 1 and 0 len lists. Source code in tests\\test_commands.py 171 172 173 174 175 176 177 178 179 @pytest . mark . parametrize ( \"blocks\" , [[], [ sha1 ( b \"1010\" ) . digest ()]]) # nosec def test_merkle_root_no_blocks ( blocks ): \"\"\" Test running merkle root function with 1 and 0 len lists. \"\"\" if blocks : assert merkle_root ( blocks ) else : assert not merkle_root ( blocks ) test_mixins_progbar ( torrent ) Test progbar mixins with small file. Source code in tests\\test_commands.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 @pytest . mark . parametrize ( \"torrent\" , torrents ()) def test_mixins_progbar ( torrent ): \"\"\" Test progbar mixins with small file. \"\"\" tfile = tempfile ( exp = 14 ) msg = \"1234abcd\" * 80 with open ( tfile , \"wb\" ) as temp : temp . write ( msg . encode ( \"utf-8\" )) args = { \"path\" : tfile , \"--prog\" : \"1\" , } metafile = torrent ( ** args ) output , _ = metafile . write () assert output == str ( tfile ) + \".torrent\" rmpath ( tfile ) test_edit Testing the edit torrent feature. test_edit_cli ( metafile2 , comment , source , announce , webseed , httpseed ) Test edit torrent with all params on cli. Source code in tests\\test_edit.py 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 @pytest . mark . parametrize ( \"comment\" , [ \"commenta\" , \"commentb\" , \"commentc\" ]) @pytest . mark . parametrize ( \"source\" , [ \"sourcea\" , \"sourceb\" , \"sourcec\" ]) @pytest . mark . parametrize ( \"announce\" , [[ \"url1\" , \"url2\" , \"url3\" ], [ \"url1\" ]]) @pytest . mark . parametrize ( \"webseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) @pytest . mark . parametrize ( \"httpseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) def test_edit_cli ( metafile2 , comment , source , announce , webseed , httpseed ): \"\"\" Test edit torrent with all params on cli. \"\"\" sys . argv = [ \"torrentfile\" , \"edit\" , metafile2 , \"--comment\" , comment , \"--source\" , source , \"--web-seed\" , webseed , \"--http-seed\" , httpseed , \"--tracker\" , announce , \"--private\" , ] main () meta = pyben . load ( metafile2 ) info = meta [ \"info\" ] assert comment == info . get ( \"comment\" ) assert source == info . get ( \"source\" ) assert info . get ( \"private\" ) == 1 assert meta [ \"announce-list\" ] == [[ announce ]] assert meta [ \"url-list\" ] == [ webseed ] test_edit_comment ( metafile2 , comment ) Test edit torrent with comment param. Source code in tests\\test_edit.py 118 119 120 121 122 123 124 125 126 127 @pytest . mark . parametrize ( \"comment\" , [ \"COMMENT\" , \"COMIT\" , \"MITCO\" ]) def test_edit_comment ( metafile2 , comment ): \"\"\" Test edit torrent with comment param. \"\"\" edits = { \"comment\" : comment } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"comment\" ] == comment test_edit_httpseeds ( metafile2 , httpseed ) Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 104 105 106 107 108 109 110 111 112 113 114 115 @pytest . mark . parametrize ( \"httpseed\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_httpseeds ( metafile2 , httpseed ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"httpseeds\" : httpseed } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseed test_edit_httpseeds_str ( metafile2 , httpseeds ) Test edit torrent with webseed param. Source code in tests\\test_edit.py 78 79 80 81 82 83 84 85 86 87 @pytest . mark . parametrize ( \"httpseeds\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_httpseeds_str ( metafile2 , httpseeds ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"httpseeds\" : httpseeds } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseeds . split () test_edit_none ( metafile2 ) Test edit torrent with None for all params. Source code in tests\\test_edit.py 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 def test_edit_none ( metafile2 ): \"\"\" Test edit torrent with None for all params. \"\"\" edits = { \"announce\" : None , \"url-list\" : None , \"comment\" : None , \"source\" : None , \"private\" : None , } data = pyben . load ( metafile2 ) edited = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta == edited test_edit_private_false ( metafile2 ) Test edit torrent with private param False. Source code in tests\\test_edit.py 153 154 155 156 157 158 159 160 161 def test_edit_private_false ( metafile2 ): \"\"\" Test edit torrent with private param False. \"\"\" edits = { \"private\" : \"\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert \"private\" not in data [ \"info\" ] test_edit_private_true ( metafile2 ) Test edit torrent with private param. Source code in tests\\test_edit.py 142 143 144 145 146 147 148 149 150 def test_edit_private_true ( metafile2 ): \"\"\" Test edit torrent with private param. \"\"\" edits = { \"private\" : \"1\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"private\" ] == 1 test_edit_removal ( metafile2 ) Test edit torrent with empty for all params. Source code in tests\\test_edit.py 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_edit_removal ( metafile2 ): \"\"\" Test edit torrent with empty for all params. \"\"\" edits = { \"announce\" : \"\" , \"url-list\" : \"\" , \"httpseeds\" : \"\" , \"comment\" : \"\" , \"source\" : \"\" , \"private\" : \"\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta test_edit_source ( metafile2 , source ) Test edit torrent with source param. Source code in tests\\test_edit.py 130 131 132 133 134 135 136 137 138 139 @pytest . mark . parametrize ( \"source\" , [ \"SomeSource\" , \"NoSouce\" , \"MidSource\" ]) def test_edit_source ( metafile2 , source ): \"\"\" Test edit torrent with source param. \"\"\" edits = { \"source\" : source } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"source\" ] == source test_edit_torrent ( metafile2 , announce ) Test edit torrent with announce param. Source code in tests\\test_edit.py 40 41 42 43 44 45 46 47 48 49 50 51 @pytest . mark . parametrize ( \"announce\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_torrent ( metafile2 , announce ): \"\"\" Test edit torrent with announce param. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce ] test_edit_torrent_str ( metafile2 , announce ) Test edit torrent with announce param as string. Source code in tests\\test_edit.py 54 55 56 57 58 59 60 61 62 63 @pytest . mark . parametrize ( \"announce\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_torrent_str ( metafile2 , announce ): \"\"\" Test edit torrent with announce param as string. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce . split ()] test_edit_urllist ( metafile2 , url_list ) Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 90 91 92 93 94 95 96 97 98 99 100 101 @pytest . mark . parametrize ( \"url_list\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_urllist ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list test_edit_urllist_str ( metafile2 , url_list ) Test edit torrent with webseed param. Source code in tests\\test_edit.py 66 67 68 69 70 71 72 73 74 75 @pytest . mark . parametrize ( \"url_list\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_urllist_str ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list . split () test_fix () Testing dir fixtures. Source code in tests\\test_edit.py 33 34 35 36 37 def test_fix (): \"\"\" Testing dir fixtures. \"\"\" assert dir2 and metafile2 and dir1 test_metafile_edit_with_unicode ( metafile2 ) Test if editing full unicode works as it should. Source code in tests\\test_edit.py 233 234 235 236 237 238 239 240 241 242 243 244 245 246 def test_metafile_edit_with_unicode ( metafile2 ): \"\"\" Test if editing full unicode works as it should. \"\"\" edits = { \"comment\" : \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"source\" : \"\u4e02\u4e03\u4e07\u4e0f\u4e11\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) com1 = data [ \"info\" ][ \"comment\" ] com2 = meta [ \"info\" ][ \"comment\" ] msg = edits [ \"comment\" ] assert com1 == com2 == msg test_interactive Testing functions for the command line interface. test_fixtures () Test the fixtures used in module. Source code in tests\\test_interactive.py 37 38 39 40 41 def test_fixtures (): \"\"\" Test the fixtures used in module. \"\"\" assert filemeta2 and file1 and file2 test_inter_create_full ( file1 , piece_length , announce , comment , source , url_list , version , monkeypatch ) Test creating torrent interactively with many parameters. Source code in tests\\test_interactive.py 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"piece_length\" , [ \"23\" , \"18\" , \"131072\" ]) @pytest . mark . parametrize ( \"announce\" , [ \"url1\" , \"urla urlb urlc\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Do\" , \"Ra\" , \"Me\" ]) def test_inter_create_full ( file1 , piece_length , announce , comment , source , url_list , version , monkeypatch , ): \"\"\" Test creating torrent interactively with many parameters. \"\"\" mapping = [ \"create\" , piece_length , announce , url_list , url_list , comment , source , \"Y\" , file1 , str ( file1 ) + \".torrent\" , version , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta = pyben . load ( str ( file1 ) + \".torrent\" ) assert meta [ \"info\" ][ \"source\" ] == source assert meta [ \"info\" ][ \"piece length\" ] == normalize_piece_length ( piece_length ) assert meta [ \"info\" ][ \"comment\" ] == comment assert meta [ \"url-list\" ] == url_list . split () test_inter_edit_cli ( filemeta2 , announce , cmnt , srce , urllist , monkeypatch ) Test editing torrent interactively from CLI. Source code in tests\\test_interactive.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 @pytest . mark . parametrize ( \"announce\" , [ \"urla urlb urlc\" , \"urld url2\" ]) @pytest . mark . parametrize ( \"urllist\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"cmnt\" , [ \"Some Comment\" ]) @pytest . mark . parametrize ( \"srce\" , [ \"Do\" , \"Ra\" ]) def test_inter_edit_cli ( filemeta2 , announce , cmnt , srce , urllist , monkeypatch ): \"\"\" Test editing torrent interactively from CLI. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , cmnt , \"2\" , srce , \"5\" , urllist , urllist , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) sys . argv = [ \"torrentfile\" , \"-i\" ] main () meta2 = pyben . load ( filemeta2 ) assert meta2 [ \"info\" ][ \"source\" ] == srce assert meta2 [ \"info\" ][ \"comment\" ] == cmnt assert meta2 [ \"url-list\" ] == urllist . split () assert meta2 [ \"info\" ][ \"private\" ] == 1 test_inter_edit_full ( filemeta2 , announce , comment , source , url_list , monkeypatch ) Test editing torrent file interactively. Source code in tests\\test_interactive.py 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 @pytest . mark . parametrize ( \"announce\" , [ \"url1\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Fa\" , \"So\" , \"La\" ]) def test_inter_edit_full ( filemeta2 , announce , comment , source , url_list , monkeypatch ): \"\"\" Test editing torrent file interactively. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , comment , \"2\" , source , \"5\" , url_list , \"\" , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta1 = pyben . load ( filemeta2 ) assert meta1 [ \"info\" ][ \"source\" ] == source assert meta1 [ \"info\" ][ \"comment\" ] == comment assert meta1 [ \"url-list\" ] == url_list . split () assert meta1 [ \"info\" ][ \"private\" ] == 1 test_inter_recheck ( torrentclass , monkeypatch , file1 ) Test interactive recheck function. Source code in tests\\test_interactive.py 180 181 182 183 184 185 186 187 188 189 190 191 @pytest . mark . parametrize ( \"torrentclass\" , torrents ()) def test_inter_recheck ( torrentclass , monkeypatch , file1 ): \"\"\" Test interactive recheck function. \"\"\" torrent = torrentclass ( path = file1 ) filemeta , _ = torrent . write () seq = [ \"recheck\" , filemeta , str ( file1 )] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) result = select_action () assert result == 100 test_interactive_create ( monkeypatch , file1 ) Test creating torrent interactively. Source code in tests\\test_interactive.py 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 def test_interactive_create ( monkeypatch , file1 ): \"\"\" Test creating torrent interactively. \"\"\" mapping = [ \"create\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , file1 , str ( file1 ) + \".torrent\" , \"\" , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () assert os . path . exists ( str ( file1 ) + \".torrent\" ) test_recheck Testing functions for the progress module. test_checker_callback ( dir1 , metafile1 ) Test Checker class with directory that points to nothing. Source code in tests\\test_recheck.py 125 126 127 128 129 130 131 def test_checker_callback ( dir1 , metafile1 ): \"\"\" Test Checker class with directory that points to nothing. \"\"\" Checker . register_callback ( lambda * x : print ( x )) checker = Checker ( metafile1 , str ( dir1 )) assert checker . results () == 100 test_checker_class ( dir1 , metafile1 ) Test Checker Class against meta files. Source code in tests\\test_recheck.py 42 43 44 45 46 47 def test_checker_class ( dir1 , metafile1 ): \"\"\" Test Checker Class against meta files. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100 test_checker_class_allfiles ( sizedfiles , dir2 ) Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 def test_checker_class_allfiles ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" def traverse ( path ): \"\"\" Traverse internal subdirectories. \"\"\" if path . is_file (): rmpath ( path ) elif path . is_dir (): for item in path . iterdir (): traverse ( item ) traverse ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_class_allpaths ( sizedfiles , dir2 ) Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 211 212 213 214 215 216 217 218 def test_checker_class_allpaths ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" for item in Path ( str ( dir2 )) . iterdir (): rmpath ( item ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_class_half_file ( filemeta2 , file2 ) Test Checker class with half size single file. Source code in tests\\test_recheck.py 221 222 223 224 225 226 227 228 229 230 231 232 def test_checker_class_half_file ( filemeta2 , file2 ): \"\"\" Test Checker class with half size single file. \"\"\" half = int ( os . path . getsize ( file2 ) / 2 ) barr = bytearray ( half ) with open ( file2 , \"rb\" ) as content : content . readinto ( barr ) with open ( file2 , \"wb\" ) as content : content . write ( barr ) checker = Checker ( filemeta2 , file2 ) assert int ( checker . results ()) != 10 test_checker_cli_args ( dir1 , metafile1 ) Test exclusive Checker Mode CLI. Source code in tests\\test_recheck.py 134 135 136 137 138 139 140 def test_checker_cli_args ( dir1 , metafile1 ): \"\"\" Test exclusive Checker Mode CLI. \"\"\" sys . argv = [ \"torrentfile\" , \"check\" , str ( metafile1 ), str ( dir1 )] output = main () assert output == 100 test_checker_empty_files ( dir2 , sizedfiles ) Test Checker when directory contains 0 length files. Source code in tests\\test_recheck.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 def test_checker_empty_files ( dir2 , sizedfiles ): \"\"\" Test Checker when directory contains 0 length files. \"\"\" def empty_files ( root ): \"\"\" Dump contents of files. \"\"\" if os . path . isfile ( root ): with open ( root , \"wb\" ) as _ : pass assert os . path . getsize ( root ) == 0 elif os . path . isdir ( root ): for item in os . listdir ( root ): return empty_files ( os . path . join ( root , item )) return root empty_files ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100 test_checker_first_piece ( dir2 , sizedfiles ) Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 def test_checker_first_piece ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if path . is_file (): new = b \"Something other than what was there before.\" with open ( path , \"rb\" ) as bfile : data = bfile . read () new_len = len ( new ) content = b \"\" . join ([ new , data [ new_len :]]) with open ( path , \"wb\" ) as bdoc : bdoc . write ( content ) elif path . is_dir (): for item in path . iterdir (): change ( item ) change ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100 test_checker_first_piece_alt ( dir2 , sizedfiles ) Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 def test_checker_first_piece_alt ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if os . path . isfile ( path ): with open ( path , \"rb\" ) as bfile : data = bfile . read () new = b \"some_other_bytes_to_use\" new_len = len ( new ) with open ( path , \"wb\" ) as wfile : wfile . write ( new + data [ new_len :]) elif os . path . isdir ( path ): for item in os . listdir ( path ): change ( os . path . join ( path , item )) change ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100 test_checker_missing ( sizedfiles , dir2 ) Test Checker class when files are missing from contents. Source code in tests\\test_recheck.py 179 180 181 182 183 184 185 186 187 188 def test_checker_missing ( sizedfiles , dir2 ): \"\"\" Test Checker class when files are missing from contents. \"\"\" count = 0 for fd in Path ( dir2 ) . iterdir (): if fd . is_file () and count < 2 : rmpath ( fd ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_missing_singles ( dir2 , sizedfiles ) Test Checker class with half size single file. Source code in tests\\test_recheck.py 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def test_checker_missing_singles ( dir2 , sizedfiles ): \"\"\" Test Checker class with half size single file. \"\"\" def walk ( root ): \"\"\" Remove first file found. \"\"\" if root . is_file (): rmpath ( root ) return True if root . is_dir (): for item in root . iterdir (): walk ( item ) return False walk ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100 test_checker_no_meta_file () Test Checker when incorrect metafile is provided. Source code in tests\\test_recheck.py 159 160 161 162 163 164 165 166 def test_checker_no_meta_file (): \"\"\" Test Checker when incorrect metafile is provided. \"\"\" try : Checker ( \"peaches\" , \"$\" ) except FileNotFoundError : assert True test_checker_parent_dir ( dir1 , metafile1 ) Test providing the parent directory for torrent checking feature. Source code in tests\\test_recheck.py 143 144 145 146 147 148 def test_checker_parent_dir ( dir1 , metafile1 ): \"\"\" Test providing the parent directory for torrent checking feature. \"\"\" checker = Checker ( metafile1 , os . path . dirname ( dir1 )) assert checker . results () == 100 test_checker_result_property ( dir1 , metafile1 ) Test Checker class with half size single file. Source code in tests\\test_recheck.py 257 258 259 260 261 262 263 def test_checker_result_property ( dir1 , metafile1 ): \"\"\" Test Checker class with half size single file. \"\"\" checker = Checker ( metafile1 , dir1 ) result = checker . results () assert checker . results () == result test_checker_simplest ( dir1 , metafile1 ) Test the simplest example. Source code in tests\\test_recheck.py 266 267 268 269 270 271 def test_checker_simplest ( dir1 , metafile1 ): \"\"\" Test the simplest example. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100 test_checker_with_file ( file1 , filemeta1 ) Test checker with single file torrent. Source code in tests\\test_recheck.py 151 152 153 154 155 156 def test_checker_with_file ( file1 , filemeta1 ): \"\"\" Test checker with single file torrent. \"\"\" checker = Checker ( filemeta1 , file1 ) assert checker . results () == 100 test_checker_wrong_root_dir ( metafile1 ) Test Checker when incorrect root directory is provided. Source code in tests\\test_recheck.py 169 170 171 172 173 174 175 176 def test_checker_wrong_root_dir ( metafile1 ): \"\"\" Test Checker when incorrect root directory is provided. \"\"\" try : Checker ( metafile1 , \"fake\" ) except FileNotFoundError : assert True test_fixtures () Test fixtures exist. Source code in tests\\test_recheck.py 33 34 35 36 37 38 39 def test_fixtures (): \"\"\" Test fixtures exist. \"\"\" assert dir1 and dir2 and file1 and file2 assert filemeta1 and filemeta2 and metafile1 assert metafile2 and sizes and sizedfiles test_partial_metafiles ( dir2 , sizedfiles ) Test Checker with data that is expected to be incomplete. Source code in tests\\test_recheck.py 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 def test_partial_metafiles ( dir2 , sizedfiles ): \"\"\" Test Checker with data that is expected to be incomplete. \"\"\" def shortenfile ( path ): \"\"\" Shorten a few files for testing purposes. \"\"\" with open ( path , \"rb\" ) as bfile : data = bfile . read () with open ( path , \"wb\" ) as bfile : bfile . write ( data [: - ( 2 ** 10 )]) for item in os . listdir ( dir2 ): full = os . path . join ( dir2 , item ) if os . path . isfile ( full ): shortenfile ( full ) testdir = os . path . dirname ( dir2 ) checker = Checker ( sizedfiles , testdir ) assert checker . results () != 100 test_recheck_wrong_dir ( metafile1 ) Test recheck function with directory that doesn't contain the contents. Source code in tests\\test_recheck.py 297 298 299 300 301 302 303 304 305 def test_recheck_wrong_dir ( metafile1 ): \"\"\" Test recheck function with directory that doesn't contain the contents. \"\"\" grandparent = os . path . dirname ( os . path . dirname ( metafile1 )) try : _ = Checker ( metafile1 , grandparent ) except FileNotFoundError : assert True test_torrent Testing functions for the torrent module. test_create_cwd_fail () Test cwd argument with create command failure. Source code in tests\\test_torrent.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_create_cwd_fail (): \"\"\"Test cwd argument with create command failure.\"\"\" class SuFile : \"\"\"A mock admin file.\"\"\" @staticmethod def __fspath__ (): raise PermissionError def __str__ ( self ): return \"SuFile\" tfile = tempfile () name = os . path . basename ( tfile ) + \".torrent\" torrent = MetaFile ( path = tfile ) sufile = SuFile () torrent . write ( outfile = sufile ) current = os . path . join ( \".\" , name ) assert os . path . exists ( current ) rmpath ( tfile , current ) test_fixtures () Test pytest fixtures. Source code in tests\\test_torrent.py 32 33 34 35 36 def test_fixtures (): \"\"\" Test pytest fixtures. \"\"\" assert dir1 and dir2 test_mbtorrent ( version , progress ) Test torrent creation for file size larger than 10MB. Source code in tests\\test_torrent.py 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 @pytest . mark . parametrize ( \"version\" , torrents ()) @pytest . mark . parametrize ( \"progress\" , [ 0 , 1 , 2 ]) def test_mbtorrent ( version , progress ): \"\"\" Test torrent creation for file size larger than 10MB. \"\"\" tfile = tempfile ( exp = 26 ) args = { \"path\" : tfile , \"progress\" : progress , \"piece_length\" : \"14\" , } torrent = version ( ** args ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile ) test_metafile_assemble ( dir1 ) Test assembling base metafile exception. Source code in tests\\test_torrent.py 50 51 52 53 54 55 56 57 58 def test_metafile_assemble ( dir1 ): \"\"\" Test assembling base metafile exception. \"\"\" metafile = MetaFile ( path = dir1 ) try : metafile . assemble () except NotImplementedError : assert True test_torrentfile_extra ( dir2 , version ) Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_extra ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" def walk ( item ): \"\"\" Edit files in directory structure. \"\"\" if item . is_file (): with open ( item , \"ab\" ) as binfile : binfile . write ( bytes ( 1000 )) elif item . is_dir (): for sub in item . iterdir (): walk ( sub ) walk ( dir2 ) args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\" test_torrentfile_missing_path ( version ) Test missing path error exception. Source code in tests\\test_torrent.py 39 40 41 42 43 44 45 46 47 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_missing_path ( version ): \"\"\" Test missing path error exception. \"\"\" try : version () except MissingPathError : assert True test_torrentfile_one_empty ( dir2 , version ) Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_one_empty ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" a = next ( os . walk ( dir2 )) if len ( a [ - 1 ]) > 0 : with open ( os . path . join ( a [ 0 ], a [ - 1 ][ 0 ]), \"w\" , encoding = \"utf-8\" ) as _ : pass args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\" test_torrentfile_single ( version , num , piece_length , capsys ) Test creating a torrent file from a single file contents. Source code in tests\\test_torrent.py 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 @pytest . mark . parametrize ( \"num\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single ( version , num , piece_length , capsys ): \"\"\" Test creating a torrent file from a single file contents. \"\"\" tfile = tempfile ( exp = num ) with capsys . disabled (): version . set_callback ( print ) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } trent = version ( ** args ) trent . write () assert os . path . exists ( str ( tfile ) + \".torrent\" ) rmpath ( tfile , str ( tfile ) + \".torrent\" ) test_torrentfile_single_extra ( version , size , piece_length ) Test creating a torrent file from a single file contents plus extra. Source code in tests\\test_torrent.py 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 @pytest . mark . parametrize ( \"size\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single_extra ( version , size , piece_length ): \"\"\" Test creating a torrent file from a single file contents plus extra. \"\"\" tfile = tempfile ( exp = size ) with open ( tfile , \"ab\" ) as binfile : binfile . write ( bytes ( str ( tfile ) . encode ( \"utf-8\" ))) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } torrent = version ( ** args ) torrent . write () outfile = str ( tfile ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( tfile , outfile ) test_torrentfile_single_under ( ver , sze , piecelength ) Test creating a torrent file from less than a single file contents. Source code in tests\\test_torrent.py 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 @pytest . mark . parametrize ( \"sze\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piecelength\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"ver\" , torrents ()) def test_torrentfile_single_under ( ver , sze , piecelength ): \"\"\" Test creating a torrent file from less than a single file contents. \"\"\" tfile = tempfile ( exp = sze ) with open ( tfile , \"rb\" ) as binfile : data = binfile . read () with open ( tfile , \"wb\" ) as binfile : binfile . write ( data [: - ( 2 ** 9 )]) kwargs = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piecelength , } torrent = ver ( ** kwargs ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile ) test_waiting_mixin () Test waiting function. Source code in tests\\test_torrent.py 198 199 200 201 202 203 204 205 206 def test_waiting_mixin (): \"\"\" Test waiting function. \"\"\" msg = \"Testing message\" lst = [] timeout = 3 waiting ( msg , lst , timeout = timeout ) assert len ( lst ) == 0 test_utils Unittest functions for testing torrentfile utils module. test_filelist_total ( dir1 ) Test function for acquiring a filelist for directory. Source code in tests\\test_utils.py 118 119 120 121 122 123 def test_filelist_total ( dir1 ): \"\"\" Test function for acquiring a filelist for directory. \"\"\" total , _ = utils . filelist_total ( dir1 ) assert total == ( 2 ** 18 ) * 8 test_filelisttotal_missing ( dir2 ) Test function filelist total with missing path. Parameters: Name Type Description Default dir2 pytest . fixture fixture containing a temporary directory required Source code in tests\\test_utils.py 225 226 227 228 229 230 231 232 233 234 235 236 237 def test_filelisttotal_missing ( dir2 ): \"\"\"Test function filelist total with missing path. Parameters ---------- dir2 : pytest.fixture fixture containing a temporary directory \"\"\" rmpath ( dir2 ) try : utils . filelist_total ( dir2 ) except utils . MissingPathError : assert True test_get_filelist ( dir1 ) Test function for get a list of files in a directory. Source code in tests\\test_utils.py 102 103 104 105 106 107 def test_get_filelist ( dir1 ): \"\"\" Test function for get a list of files in a directory. \"\"\" filelist = utils . get_file_list ( dir1 ) assert len ( filelist ) == 8 test_get_path_length_max ( dir1 ) Test function for getting piece length for folders max. Source code in tests\\test_utils.py 71 72 73 74 75 def test_get_path_length_max ( dir1 ): \"\"\" Test function for getting piece length for folders max. \"\"\" assert utils . path_piece_length ( dir1 ) <= ( 2 ** 27 ) test_get_path_length_min ( dir1 ) Test function for getting piece length for folders min. Source code in tests\\test_utils.py 64 65 66 67 68 def test_get_path_length_min ( dir1 ): \"\"\" Test function for getting piece length for folders min. \"\"\" assert utils . path_piece_length ( dir1 ) >= ( 2 ** 14 ) test_get_path_length_mod ( dir1 ) Test function for the best piece length for provided path. Source code in tests\\test_utils.py 57 58 59 60 61 def test_get_path_length_mod ( dir1 ): \"\"\" Test function for the best piece length for provided path. \"\"\" assert utils . path_piece_length ( dir1 ) % ( 2 ** 14 ) == 0 test_get_path_size ( dir1 ) Test function for getting total size of directory. Source code in tests\\test_utils.py 110 111 112 113 114 115 def test_get_path_size ( dir1 ): \"\"\" Test function for getting total size of directory. \"\"\" pathsize = utils . path_size ( dir1 ) assert pathsize == ( 2 ** 18 ) * 8 test_get_piece_length ( size ) Test function for best piece length for given size. Source code in tests\\test_utils.py 30 31 32 33 34 35 36 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length ( size ): \"\"\" Test function for best piece length for given size. \"\"\" value = utils . get_piece_length ( size ) assert value % 1024 == 0 test_get_piece_length_max ( size ) Test function for best piece length for given size maximum. Source code in tests\\test_utils.py 39 40 41 42 43 44 45 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_max ( size ): \"\"\" Test function for best piece length for given size maximum. \"\"\" value = utils . get_piece_length ( size ) assert value < 2 ** 27 test_get_piece_length_min ( size ) Test function for best piece length for given size minimum. Source code in tests\\test_utils.py 48 49 50 51 52 53 54 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_min ( size ): \"\"\" Test function for best piece length for given size minimum. \"\"\" value = utils . get_piece_length ( size ) assert value >= 2 ** 14 test_humanize_bytes ( amount , result ) Test humanize bytes function. Source code in tests\\test_utils.py 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 @pytest . mark . parametrize ( \"amount, result\" , [ ( 100 , \"100\" ), ( 1100 , \"1 KiB\" ), ( 1_100_000 , \"1 MiB\" ), ( 1_100_000_000 , \"1 GiB\" ), ( 4_400_120_000 , \"4 GiB\" ), ( 4_000_120_000 , \"3 GiB\" ), ], ) def test_humanize_bytes ( amount , result ): \"\"\" Test humanize bytes function. \"\"\" assert utils . humanize_bytes ( amount ) == result test_missing_path_error () Test exception for missing path parameter. Source code in tests\\test_utils.py 137 138 139 140 141 142 143 144 145 def test_missing_path_error (): \"\"\" Test exception for missing path parameter. \"\"\" try : raise utils . MissingPathError ( \"message\" ) except utils . MissingPathError : assert True assert dir2 test_next_power_2 ( value ) Test next power of 2 function in utils module. Source code in tests\\test_utils.py 148 149 150 151 152 153 154 155 156 157 @pytest . mark . parametrize ( \"value\" , [ 5 , 32 , 18 , 225 , 16384 , 256000 ]) def test_next_power_2 ( value ): \"\"\" Test next power of 2 function in utils module. \"\"\" result = utils . next_power_2 ( value ) log = math . log2 ( result ) assert log == int ( log ) assert result % 2 == 0 assert result >= value test_norm_plength_errors ( amount ) Test function to normalize piece length errors. Parameters: Name Type Description Default amount any arguments intended to raise an exception. required Source code in tests\\test_utils.py 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"amount\" , [ \"hello\" , 11 , 0 , 100000 , 28 , \"zero\" , \"fifteen\" ] ) def test_norm_plength_errors ( amount ): \"\"\"Test function to normalize piece length errors. Parameters ---------- amount : any arguments intended to raise an exception. \"\"\" try : assert utils . normalize_piece_length ( amount ) except utils . PieceLengthValueError : assert True test_normalize_piece_length_int ( amount , result ) Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 178 179 180 181 182 183 184 185 186 187 188 189 @pytest . mark . parametrize ( \"amount, result\" , [( i , 2 ** i ) for i in range ( 14 , 25 )]) def test_normalize_piece_length_int ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result test_normalize_piece_length_str ( amount , result ) Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 192 193 194 195 196 197 198 199 200 201 202 203 204 205 @pytest . mark . parametrize ( \"amount, result\" , [( str ( i ), 2 ** i ) for i in range ( 14 , 21 )] ) def test_normalize_piece_length_str ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result test_path_stat ( dir1 ) Test function for acquiring piece length information on folder. Source code in tests\\test_utils.py 78 79 80 81 82 83 def test_path_stat ( dir1 ): \"\"\" Test function for acquiring piece length information on folder. \"\"\" _ , _ , piece_length = utils . path_stat ( dir1 ) assert piece_length % ( 2 ** 14 ) == 0 test_path_stat_filelist_size ( dir1 ) Test function for acquiring file list information on folder. Source code in tests\\test_utils.py 94 95 96 97 98 99 def test_path_stat_filelist_size ( dir1 ): \"\"\" Test function for acquiring file list information on folder. \"\"\" filelist , _ , _ = utils . path_stat ( dir1 ) assert len ( filelist ) == 8 test_path_stat_size ( dir1 ) Test function for acquiring total size information on folder. Source code in tests\\test_utils.py 86 87 88 89 90 91 def test_path_stat_size ( dir1 ): \"\"\" Test function for acquiring total size information on folder. \"\"\" _ , totalsize , _ = utils . path_stat ( dir1 ) assert totalsize == ( 2 ** 18 ) * 8 test_piecelength_error_fixtures () Test exception for uninterpretable piece length value. Source code in tests\\test_utils.py 126 127 128 129 130 131 132 133 134 def test_piecelength_error_fixtures (): \"\"\" Test exception for uninterpretable piece length value. \"\"\" try : raise utils . PieceLengthValueError ( \"message\" ) except utils . PieceLengthValueError : assert True assert dir1","title":"Source Code"},{"location":"source/#torrentfile_1","text":"","title":"TorrentFile"},{"location":"source/#api-and-source","text":"","title":"API and Source"},{"location":"source/#torrent-module","text":"module torrentfile. torrent Classes and procedures pertaining to the creation of torrent meta files.","title":"Torrent Module"},{"location":"source/#classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. Classes MetaFile \u2014 Base Class for all TorrentFile classes. TorrentFile \u2014 Class for creating Bittorrent meta files. TorrentFileV2 \u2014 Class for creating Bittorrent meta v2 files. TorrentFileHybrid \u2014 Construct the Hybrid torrent meta file with provided parameters. TorrentAssembler \u2014 Assembler class for Bittorrent version 2 and hybrid meta files.","title":"v1 meta-dictionary"},{"location":"source/#-","text":"","title":"------"},{"location":"source/#utils-module","text":"module torrentfile. utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Classes Memo \u2014 Memoice chache object. MissingPathError \u2014 Path parameter is required to specify target content. PieceLengthValueError \u2014 Piece Length parameter must equal a perfect power of 2. Functions get_file_list ( path ) (list) \u2014 Return a sorted list of file paths contained in directory. get_piece_length ( size ) (int) \u2014 Calculate the ideal piece length for bittorrent data. humanize_bytes ( amount ) (str) \u2014 Convert integer into human readable memory sized denomination. next_power_2 ( value ) (int) \u2014 Calculate the next perfect power of 2 equal to or greater than value. normalize_piece_length ( piece_length ) (int) \u2014 Verify input piece_length is valid and convert accordingly. path_piece_length ( path ) (int) \u2014 Calculate piece length for input path and contents. path_size ( path ) (int) \u2014 Return the total size of all files in path recursively. path_stat ( path ) (list) \u2014 Calculate directory statistics.","title":"Utils Module"},{"location":"source/#-_1","text":"","title":"------"},{"location":"source/#edit-module","text":"module torrentfile. edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"Edit Module"},{"location":"source/#keywords","text":"private comment source trackers web-seeds Functions edit_torrent ( metafile , args ) (dict) \u2014 Edit the properties and values in a torrent meta file. filter_empty ( args , meta , info ) \u2014 Remove dictionary keys with empty values.","title":"Keywords"},{"location":"source/#-_2","text":"","title":"------"},{"location":"source/#interactive-module","text":"module torrentfile. interactive Module contains the procedures used for Interactive Mode. Classes InteractiveEditor \u2014 Interactive dialog class for torrent editing. InteractiveCreator \u2014 Class namespace for interactive program options. Functions create_torrent ( ) \u2014 Create new torrent file interactively. edit_action ( ) \u2014 Edit the editable values of the torrent meta file. get_input ( *args ) (str) \u2014 Determine appropriate input function to call. recheck_torrent ( ) \u2014 Check torrent download completed percentage. select_action ( ) \u2014 Operate TorrentFile program interactively through terminal. showcenter ( txt ) \u2014 Print text to screen in the center position of the terminal. showtext ( txt ) \u2014 Print contents of txt to screen.","title":"Interactive Module"},{"location":"source/#-_3","text":"","title":"------"},{"location":"source/#cli-module","text":"module torrentfile. cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. Classes TorrentFileHelpFormatter \u2014 Formatting class for help tips provided by the CLI. Functions activate_logger ( ) \u2014 Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. main ( ) \u2014 Initiate main function for CLI script.","title":"CLI Module"},{"location":"source/#-_4","text":"","title":"------"},{"location":"source/#recheck-module","text":"module torrentfile. recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Classes Checker \u2014 Check a given file or directory to see if it matches a torrentfile. FeedChecker \u2014 Validates torrent content. HashChecker \u2014 Verify that root hashes of content files match the .torrent files.","title":"Recheck Module"},{"location":"source/#-_5","text":"","title":"------"},{"location":"source/#hasher-module","text":"module torrentfile. hasher Piece/File Hashers for Bittorrent meta file contents. Classes Hasher \u2014 Piece hasher for Bittorrent V1 files. HasherV2 \u2014 Calculate the root hash and piece layers for file contents. HasherHybrid \u2014 Calculate root and piece hashes for creating hybrid torrent file. FileHasher \u2014 Calculate root and piece hashes for creating hybrid torrent file. Functions merkle_root ( blocks ) (bytes) \u2014 Calculate the merkle root for a seq of sha256 hash digests.","title":"Hasher Module"},{"location":"source/#-_6","text":"","title":"------"},{"location":"source/#torrentfile","text":"Torrentfile can create Bittorrent metafiles for any content. Both Bittorrent v1 and v2 are fully supported. Also included is a torrent torrent file checker, which can verify a .torrent file is formated correctly as well as validate files and folders against metadata. Modules: metafile: Creation/Validation of v1 .torrent files. metafile2: Creation/Validation of v2 .torrent files. torrentfile: torrentfiles Command Line Interface implementation. exceptions: Custom Exceptions used in package. utils: Utilities used throughout package.","title":"torrentfile"},{"location":"source/#torrentfile.create","text":"Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args","title":"create()"},{"location":"source/#torrentfile.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args","title":"execute()"},{"location":"source/#torrentfile.info","text":"Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output","title":"info()"},{"location":"source/#torrentfile.magnet","text":"Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile : str ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri","title":"magnet()"},{"location":"source/#torrentfile.__main__","text":"Enable calling the package directly with python from the command line.","title":"__main__"},{"location":"source/#torrentfile.__main__.main","text":"Start the entry point script. Source code in torrentfile\\__main__.py 26 27 28 29 30 def main (): \"\"\" Start the entry point script. \"\"\" execute ()","title":"main()"},{"location":"source/#torrentfile.cli","text":"Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility.","title":"cli"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter","text":"Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"TorrentFileHelpFormatter"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.__init__","text":"Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions )","title":"__init__()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._format_text","text":"Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \"","title":"_format_text()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._join_parts","text":"Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts )","title":"_join_parts()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._split_lines","text":"Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ]","title":"_split_lines()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.format_headers","text":"Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"format_headers()"},{"location":"source/#torrentfile.cli.activate_logger","text":"Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" )","title":"activate_logger()"},{"location":"source/#torrentfile.cli.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args","title":"execute()"},{"location":"source/#torrentfile.cli.main","text":"Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute ()","title":"main()"},{"location":"source/#torrentfile.commands","text":"The commands module contains the Action Commands executed by the CLI script. Each function pertains to a command line action/subcommand and drives specific features of the application.","title":"commands"},{"location":"source/#torrentfile.commands--functions","text":"create_command info_command edit_command recheck_command magnet_command","title":"Functions"},{"location":"source/#torrentfile.commands.create","text":"Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. Source code in torrentfile\\commands.py 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def create ( args : list ): \"\"\" Execute the create CLI sub-command to create a new torrent metafile. Parameters ---------- args : argparse.Namespace positional and optional CLI arguments. Returns ------- torrentfile.MetaFile object containing the path to created metafile and its contents. \"\"\" kwargs = vars ( args ) logger . debug ( \"Creating torrent from %s \" , args . content ) if args . meta_version == \"1\" : torrent = TorrentFile ( ** kwargs ) else : torrent = TorrentAssembler ( ** kwargs ) outfile , meta = torrent . write () if args . magnet : magnet ( outfile ) args . torrent = torrent args . kwargs = kwargs args . outfile = outfile args . meta = meta print ( \" \\n Torrent Save Path: \" , os . path . abspath ( str ( outfile ))) logger . debug ( \"Output path: %s \" , str ( outfile )) return args","title":"create()"},{"location":"source/#torrentfile.commands.edit","text":"Execute the edit CLI sub-command with provided arguments. Parameters: Name Type Description Default args Namespace positional and optional CLI arguments. required Returns: Type Description str path to edited torrent file. Source code in torrentfile\\commands.py 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 def edit ( args : list ): \"\"\" Execute the edit CLI sub-command with provided arguments. Parameters ---------- args : Namespace positional and optional CLI arguments. Returns ------- str path to edited torrent file. \"\"\" metafile = args . metafile logger . info ( \"Editing %s Meta File\" , str ( args . metafile )) editargs = { \"url-list\" : args . url_list , \"httpseeds\" : args . httpseeds , \"announce\" : args . announce , \"source\" : args . source , \"private\" : args . private , \"comment\" : args . comment , } return edit_torrent ( metafile , editargs )","title":"edit()"},{"location":"source/#torrentfile.commands.info","text":"Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required Source code in torrentfile\\commands.py 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 def info ( args : list ): \"\"\" Show torrent metafile details to user via stdout. Parameters ---------- args : dict command line arguements provided by the user. \"\"\" metafile = args . metafile meta = pyben . load ( metafile ) info = meta [ \"info\" ] del meta [ \"info\" ] meta . update ( info ) if \"private\" in meta and meta [ \"private\" ] == 1 : meta [ \"private\" ] = \"True\" if \"announce-list\" in meta : lst = meta [ \"announce-list\" ] meta [ \"announce-list\" ] = \", \" . join ([ j for i in lst for j in i ]) if \"url-list\" in meta : meta [ \"url-list\" ] = \", \" . join ( meta [ \"url-list\" ]) if \"httpseeds\" in meta : meta [ \"httpseeds\" ] = \", \" . join ( meta [ \"httpseeds\" ]) text = [] longest = max ([ len ( i ) for i in meta . keys ()]) for key , val in meta . items (): if key not in [ \"pieces\" , \"piece layers\" , \"files\" , \"file tree\" ]: prefix = longest - len ( key ) + 1 string = key + ( \" \" * prefix ) + str ( val ) text . append ( string ) most = max ([ len ( i ) for i in text ]) text = [ \"-\" * most , \" \\n \" ] + text + [ \" \\n \" , \"-\" * most ] output = \" \\n \" . join ( text ) print ( output ) return output","title":"info()"},{"location":"source/#torrentfile.commands.magnet","text":"Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. Source code in torrentfile\\commands.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 def magnet ( metafile : str ): \"\"\" Create a magnet URI from a Bittorrent meta file. Parameters ---------- metafile : (Namespace||str) Namespace class for CLI arguments. Returns ------- str created magnet URI. \"\"\" if hasattr ( metafile , \"metafile\" ): metafile = metafile . metafile if not os . path . exists ( metafile ): raise FileNotFoundError meta = pyben . load ( metafile ) info = meta [ \"info\" ] binfo = pyben . dumps ( info ) infohash = sha1 ( binfo ) . hexdigest () . upper () # nosec logger . info ( \"Magnet Info Hash: %s \" , infohash ) scheme = \"magnet:\" hasharg = \"?xt=urn:btih:\" + infohash namearg = \"&dn=\" + quote_plus ( info [ \"name\" ]) if \"announce-list\" in meta : announce_args = [ \"&tr=\" + quote_plus ( url ) for urllist in meta [ \"announce-list\" ] for url in urllist ] else : announce_args = [ \"&tr=\" + quote_plus ( meta [ \"announce\" ])] full_uri = \"\" . join ([ scheme , hasharg , namearg ] + announce_args ) logger . info ( \"Created Magnet URI %s \" , full_uri ) sys . stdout . write ( \" \\n \" + full_uri + \" \\n \" ) return full_uri","title":"magnet()"},{"location":"source/#torrentfile.commands.recheck","text":"Execute recheck CLI sub-command. Parameters: Name Type Description Default args Namespace positional and optional arguments. required Returns: Type Description str The percentage of content currently saved to disk. Source code in torrentfile\\commands.py 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 def recheck ( args : list ): \"\"\" Execute recheck CLI sub-command. Parameters ---------- args : Namespace positional and optional arguments. Returns ------- str The percentage of content currently saved to disk. \"\"\" metafile = args . metafile content = args . content logger . debug ( \"Validating %s against %s contents\" , metafile , content ) checker = Checker ( metafile , content ) logger . debug ( \"Completed initialization of the Checker class\" ) result = checker . results () sys . stdout . write ( str ( result ) + \"% Match \\n \" ) sys . stdout . flush () return result","title":"recheck()"},{"location":"source/#torrentfile.edit","text":"Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"edit"},{"location":"source/#torrentfile.edit--keywords","text":"private comment source trackers web-seeds","title":"Keywords"},{"location":"source/#torrentfile.edit.edit_torrent","text":"Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta","title":"edit_torrent()"},{"location":"source/#torrentfile.edit.filter_empty","text":"Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val )","title":"filter_empty()"},{"location":"source/#torrentfile.hasher","text":"Piece/File Hashers for Bittorrent meta file contents.","title":"hasher"},{"location":"source/#torrentfile.hasher.FileHasher","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"FileHasher"},{"location":"source/#torrentfile.hasher.FileHasher.__init__","text":"Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" )","title":"__init__()"},{"location":"source/#torrentfile.hasher.FileHasher.__iter__","text":"Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.FileHasher.__next__","text":"Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash","title":"__next__()"},{"location":"source/#torrentfile.hasher.FileHasher._calculate_root","text":"Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.FileHasher._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.Hasher","text":"Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"Hasher"},{"location":"source/#torrentfile.hasher.Hasher.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ]))","title":"__init__()"},{"location":"source/#torrentfile.hasher.Hasher.__iter__","text":"Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.Hasher.__next__","text":"Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"__next__()"},{"location":"source/#torrentfile.hasher.Hasher._handle_partial","text":"Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec","title":"_handle_partial()"},{"location":"source/#torrentfile.hasher.Hasher.next_file","text":"Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False","title":"next_file()"},{"location":"source/#torrentfile.hasher.HasherHybrid","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"HasherHybrid"},{"location":"source/#torrentfile.hasher.HasherHybrid.__init__","text":"Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherHybrid._calculate_root","text":"Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherHybrid._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.HasherHybrid.process_file","text":"Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.HasherV2","text":"Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"HasherV2"},{"location":"source/#torrentfile.hasher.HasherV2.__init__","text":"Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherV2._calculate_root","text":"Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherV2.process_file","text":"Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.merkle_root","text":"Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks","title":"merkle_root()"},{"location":"source/#torrentfile.interactive","text":"Module contains the procedures used for Interactive Mode.","title":"interactive"},{"location":"source/#torrentfile.interactive.InteractiveCreator","text":"Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"InteractiveCreator"},{"location":"source/#torrentfile.interactive.InteractiveCreator.__init__","text":"Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props ()","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveCreator.get_props","text":"Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"get_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor","text":"Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"InteractiveEditor"},{"location":"source/#torrentfile.interactive.InteractiveEditor.__init__","text":"Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), }","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.edit_props","text":"Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"edit_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.sanatize_response","text":"Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val","title":"sanatize_response()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.show_current","text":"Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out )","title":"show_current()"},{"location":"source/#torrentfile.interactive._get_input","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value","title":"_get_input()"},{"location":"source/#torrentfile.interactive._get_input_loop","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" )","title":"_get_input_loop()"},{"location":"source/#torrentfile.interactive.create_torrent","text":"Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator","title":"create_torrent()"},{"location":"source/#torrentfile.interactive.edit_action","text":"Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props ()","title":"edit_action()"},{"location":"source/#torrentfile.interactive.get_input","text":"Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- *args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args )","title":"get_input()"},{"location":"source/#torrentfile.interactive.recheck_torrent","text":"Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results","title":"recheck_torrent()"},{"location":"source/#torrentfile.interactive.select_action","text":"Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover","title":"select_action()"},{"location":"source/#torrentfile.interactive.showcenter","text":"Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string )","title":"showcenter()"},{"location":"source/#torrentfile.interactive.showtext","text":"Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt )","title":"showtext()"},{"location":"source/#torrentfile.mixins","text":"Collection of classes that can be used as Mixins with other base classes. Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the progress bar mixin. And any class is eligible to use the callback mixin.","title":"mixins"},{"location":"source/#torrentfile.mixins.CbMixin","text":"Mixin class to set a callback during hashing procedure. Source code in torrentfile\\mixins.py 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class CbMixin : \"\"\" Mixin class to set a callback during hashing procedure. \"\"\" _cb = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover","title":"CbMixin"},{"location":"source/#torrentfile.mixins.CbMixin.set_callback","text":"Assign a callback to the Hashing class. Parameters: Name Type Description Default func function the callback function required Source code in torrentfile\\mixins.py 40 41 42 43 44 45 46 47 48 49 50 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback to the Hashing class. Parameters ---------- func : function the callback function \"\"\" cls . _cb = func # pragma: nocover","title":"set_callback()"},{"location":"source/#torrentfile.mixins.ProgMixin","text":"Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files.","title":"ProgMixin"},{"location":"source/#torrentfile.mixins.ProgMixin--methods","text":"prog_start prog_update prog_close Source code in torrentfile\\mixins.py 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 class ProgMixin : \"\"\" Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods ------- prog_start prog_update prog_close \"\"\" def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start ) def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush () def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False","title":"Methods"},{"location":"source/#torrentfile.mixins.ProgMixin.is_active","text":"Test to see if there is an active progress bar for object. Returns: Name Type Description bool True if there is, otherwise False. Source code in torrentfile\\mixins.py 199 200 201 202 203 204 205 206 207 208 209 210 def is_active ( self ): \"\"\" Test to see if there is an active progress bar for object. Returns ------- bool : True if there is, otherwise False. \"\"\" if hasattr ( self , \"prog\" ): return True return False","title":"is_active()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_close","text":"Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. Source code in torrentfile\\mixins.py 187 188 189 190 191 192 193 194 195 196 197 def prog_close ( self ): \"\"\" Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. \"\"\" if self . is_active (): sys . stdout . flush () sys . stdout . write ( \" \\n \" ) del self . prog","title":"prog_close()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_start","text":"Generate a new progress bar for the given file path. Parameters: Name Type Description Default total int the total amount of units accumulating towards. required path str path to file being hashed. required length int the number of characters of the actual progress bar. 50 unit str the text representation of the value being measured. None Source code in torrentfile\\mixins.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 def prog_start ( self , total , path , length = 50 , unit = None ): \"\"\" Generate a new progress bar for the given file path. Parameters ---------- total : int the total amount of units accumulating towards. path : str path to file being hashed. length : int the number of characters of the actual progress bar. unit : str the text representation of the value being measured. \"\"\" title = path width = shutil . get_terminal_size () . columns if len ( str ( title )) >= width // 2 : parts = list ( Path ( title ) . parts ) while len ( \"//\" . join ( parts )) > width // 2 and len ( parts ) > 0 : del parts [ 0 ] title = os . path . join ( * parts ) length = min ( length , width // 2 ) start = width - int ( length * 1.5 ) self . prog = ProgressBar ( total , title , length , unit , start )","title":"prog_start()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_update","text":"Update progress bar with given amount of progress. Parameters: Name Type Description Default val int the number of bytes count the progress bar should increase. required Source code in torrentfile\\mixins.py 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 def prog_update ( self , val ): \"\"\" Update progress bar with given amount of progress. Parameters ---------- val : int the number of bytes count the progress bar should increase. \"\"\" if self . is_active (): self . prog . increment ( val ) pbar = self . prog . pbar () output = f \" { self . prog . prefix }{ pbar }{ self . prog . suffix } \\r \" sys . stdout . write ( output ) sys . stdout . flush ()","title":"prog_update()"},{"location":"source/#torrentfile.mixins.ProgressBar","text":"Holds the state and details of the terminal progress bars. Parameters: Name Type Description Default total int the total amount to be accumulated. required title str the subject of the progress tracker required length int the width of the progress bar required unit str the text representation incremented required Source code in torrentfile\\mixins.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 class ProgressBar : \"\"\" Holds the state and details of the terminal progress bars. Parameters ---------- total : int the total amount to be accumulated. title : str the subject of the progress tracker length : int the width of the progress bar unit : str the text representation incremented \"\"\" def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ]) def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar )","title":"ProgressBar"},{"location":"source/#torrentfile.mixins.ProgressBar.__init__","text":"Construct the progress bar object and store state of it's properties. Source code in torrentfile\\mixins.py 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 def __init__ ( self , total , title , length , unit , start ): \"\"\" Construct the progress bar object and store state of it's properties. \"\"\" self . total = total self . start = start self . length = length self . fill = chr ( 9608 ) self . empty = chr ( 9617 ) self . state = 0 self . unit = unit self . show_total = total if not unit : self . unit = \"\" # pragma: nocover elif unit == \"bytes\" : if self . total > 10000000 : self . show_total = math . floor ( self . total / 1048576 ) self . unit = \"MiB\" elif self . total > 10000 : self . show_total = math . floor ( self . total / 1024 ) self . unit = \"KiB\" self . suffix = f \"/ { self . show_total } { self . unit } \" if len ( title ) > start : title = title [: start - 1 ] padding = ( start - len ( title )) * \" \" self . prefix = \"\" . join ([ title , padding ])","title":"__init__()"},{"location":"source/#torrentfile.mixins.ProgressBar.increment","text":"Increase the state of the progress bar value. Parameters: Name Type Description Default value int the amount to increment the state by. required Source code in torrentfile\\mixins.py 96 97 98 99 100 101 102 103 104 105 def increment ( self , value ): \"\"\" Increase the state of the progress bar value. Parameters ---------- value : int the amount to increment the state by. \"\"\" self . state += value","title":"increment()"},{"location":"source/#torrentfile.mixins.ProgressBar.pbar","text":"Return the size of the filled portion of the progress bar. Returns: Name Type Description str the progress bar characters Source code in torrentfile\\mixins.py 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 def pbar ( self ): \"\"\" Return the size of the filled portion of the progress bar. Returns ------- str : the progress bar characters \"\"\" if self . state >= self . total : fill = self . length else : fill = math . ceil (( self . state / self . total ) * self . length ) empty = self . length - fill if self . unit == \"MiB\" : state = math . floor ( self . state / 1048576 ) elif self . unit == \"KiB\" : state = math . floor ( self . state / 1024 ) else : state = self . state progbar = [ \"[\" , self . fill * fill , self . empty * empty , \"] \" , str ( state )] return \"\" . join ( progbar )","title":"pbar()"},{"location":"source/#torrentfile.mixins.waiting","text":"Show loading message while thread completes processing. Parameters: Name Type Description Default msg str Message string printed before the progress bar required flag list Once flag is filled exit loop required timeout int max amount of time to run the function. 180 Source code in torrentfile\\mixins.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 def waiting ( msg , flag , timeout = 180 ): \"\"\" Show loading message while thread completes processing. Parameters ---------- msg : str Message string printed before the progress bar flag : list Once flag is filled exit loop timeout : int max amount of time to run the function. \"\"\" then = time . time () codes , fill = list ( range ( 9617 , 9620 )), chr ( 9619 ) size = idx = 0 total = shutil . get_terminal_size () . columns - len ( msg ) - 20 def output ( text ): \"\"\" Print parameter message to the console. Parameters ---------- text : str output message \"\"\" sys . stdout . write ( text ) sys . stdout . flush () output ( \" \\n \" ) while not flag : time . sleep ( 0.16 ) filled = ( fill * size ) + chr ( codes [ idx ]) + ( \" \" * ( total - size )) output ( f \" { msg } : { filled } \\r \" ) idx = idx + 1 if idx + 1 < len ( codes ) else 0 size = size + 1 if size < total else 0 if time . time () - then > timeout : break output ( \" \\n \" )","title":"waiting()"},{"location":"source/#torrentfile.recheck","text":"Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole.","title":"recheck"},{"location":"source/#torrentfile.recheck.Checker","text":"Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required","title":"Checker"},{"location":"source/#torrentfile.recheck.Checker--example","text":">> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"Example"},{"location":"source/#torrentfile.recheck.Checker.__init__","text":"Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.Checker.check_paths","text":"Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], [])","title":"check_paths()"},{"location":"source/#torrentfile.recheck.Checker.find_root","text":"Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root )","title":"find_root()"},{"location":"source/#torrentfile.recheck.Checker.iter_hashes","text":"Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"iter_hashes()"},{"location":"source/#torrentfile.recheck.Checker.log_msg","text":"Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message )","title":"log_msg()"},{"location":"source/#torrentfile.recheck.Checker.piece_checker","text":"Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker","title":"piece_checker()"},{"location":"source/#torrentfile.recheck.Checker.register_callback","text":"Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook","title":"register_callback()"},{"location":"source/#torrentfile.recheck.Checker.results","text":"Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result","title":"results()"},{"location":"source/#torrentfile.recheck.Checker.walk_file_tree","text":"Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ])","title":"walk_file_tree()"},{"location":"source/#torrentfile.recheck.FeedChecker","text":"Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"FeedChecker"},{"location":"source/#torrentfile.recheck.FeedChecker.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 335 336 337 338 339 340 341 342 343 344 345 346 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None","title":"__init__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 348 349 350 351 352 353 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__next__","text":"Yield back result of comparison. Source code in torrentfile\\recheck.py 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial )","title":"__next__()"},{"location":"source/#torrentfile.recheck.FeedChecker._gen_padding","text":"Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"_gen_padding()"},{"location":"source/#torrentfile.recheck.FeedChecker.extract","text":"Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad","title":"extract()"},{"location":"source/#torrentfile.recheck.FeedChecker.iter_pieces","text":"Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close ()","title":"iter_pieces()"},{"location":"source/#torrentfile.recheck.HashChecker","text":"Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required Source code in torrentfile\\recheck.py 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"HashChecker"},{"location":"source/#torrentfile.recheck.HashChecker.Padder","text":"Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"Padder"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__init__","text":"Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 530 531 532 533 534 535 536 537 538 539 540 541 542 543 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__iter__","text":"Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 545 546 547 548 549 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__next__","text":"Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.__init__","text":"Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 487 488 489 490 491 492 493 494 495 496 497 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 499 500 501 502 503 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.__next__","text":"Provide the result of comparison. Source code in torrentfile\\recheck.py 505 506 507 508 509 510 511 512 513 514 515 516 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.advance","text":"Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"advance()"},{"location":"source/#torrentfile.recheck.HashChecker.next_file","text":"Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False","title":"next_file()"},{"location":"source/#torrentfile.recheck.HashChecker.process_current","text":"Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err","title":"process_current()"},{"location":"source/#torrentfile.torrent","text":"Classes and procedures pertaining to the creation of torrent meta files.","title":"torrent"},{"location":"source/#torrentfile.torrent--classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#torrentfile.torrent--constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#torrentfile.torrent--bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#torrentfile.torrent--meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#torrentfile.torrent--bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#torrentfile.torrent--v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory.","title":"v1 meta-dictionary"},{"location":"source/#torrentfile.torrent.MetaFile","text":"Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"MetaFile"},{"location":"source/#torrentfile.torrent.MetaFile.__init__","text":"Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name","title":"__init__()"},{"location":"source/#torrentfile.torrent.MetaFile.assemble","text":"Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError","title":"assemble()"},{"location":"source/#torrentfile.torrent.MetaFile.set_callback","text":"Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func )","title":"set_callback()"},{"location":"source/#torrentfile.torrent.MetaFile.sort_meta","text":"Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta","title":"sort_meta()"},{"location":"source/#torrentfile.torrent.MetaFile.write","text":"Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"write()"},{"location":"source/#torrentfile.torrent.TorrentAssembler","text":"Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentAssembler"},{"location":"source/#torrentfile.torrent.TorrentAssembler.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentAssembler._traverse","text":"Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentAssembler.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFile","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- **kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"TorrentFile"},{"location":"source/#torrentfile.torrent.TorrentFile.__init__","text":"Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFile.assemble","text":"Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid","text":"Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentFileHybrid"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid._traverse","text":"Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileV2","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"TorrentFileV2"},{"location":"source/#torrentfile.torrent.TorrentFileV2.__init__","text":"Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileV2._traverse","text":"Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileV2.assemble","text":"Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers","title":"assemble()"},{"location":"source/#torrentfile.utils","text":"Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field.","title":"utils"},{"location":"source/#torrentfile.utils.Memo","text":"Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"Memo"},{"location":"source/#torrentfile.utils.Memo.__call__","text":"Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"__call__()"},{"location":"source/#torrentfile.utils.Memo.__init__","text":"Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {}","title":"__init__()"},{"location":"source/#torrentfile.utils.MissingPathError","text":"Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"MissingPathError"},{"location":"source/#torrentfile.utils.MissingPathError.__init__","text":"Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils.PieceLengthValueError","text":"Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"PieceLengthValueError"},{"location":"source/#torrentfile.utils.PieceLengthValueError.__init__","text":"Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils._filelist_total","text":"Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist )","title":"_filelist_total()"},{"location":"source/#torrentfile.utils.filelist_total","text":"Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError","title":"filelist_total()"},{"location":"source/#torrentfile.utils.get_file_list","text":"Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist","title":"get_file_list()"},{"location":"source/#torrentfile.utils.get_piece_length","text":"Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp","title":"get_piece_length()"},{"location":"source/#torrentfile.utils.humanize_bytes","text":"Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\"","title":"humanize_bytes()"},{"location":"source/#torrentfile.utils.next_power_2","text":"Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start","title":"next_power_2()"},{"location":"source/#torrentfile.utils.normalize_piece_length","text":"Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError","title":"normalize_piece_length()"},{"location":"source/#torrentfile.utils.path_piece_length","text":"Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize )","title":"path_piece_length()"},{"location":"source/#torrentfile.utils.path_size","text":"Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size","title":"path_size()"},{"location":"source/#torrentfile.utils.path_stat","text":"Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length )","title":"path_stat()"},{"location":"source/#torrentfile.version","text":"Holds the release version number.","title":"version"},{"location":"source/#torrentfile.cli","text":"Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility.","title":"cli"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter","text":"Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. Source code in torrentfile\\cli.py 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 class TorrentFileHelpFormatter ( HelpFormatter ): \"\"\" Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. \"\"\" def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions ) def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ] def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \" def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts ) @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"TorrentFileHelpFormatter"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.__init__","text":"Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 Source code in torrentfile\\cli.py 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 def __init__ ( self , prog , width = 40 , max_help_positions = 30 ): \"\"\" Construct HelpFormat class for usage output. Parameters ---------- prog : str Name of the program. width : int Max width of help message output. max_help_positions : int max length until line wrap. \"\"\" super () . __init__ ( prog , width = width , max_help_position = max_help_positions )","title":"__init__()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._format_text","text":"Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. Source code in torrentfile\\cli.py 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 def _format_text ( self , text ): \"\"\" Format text for cli usage messages. Parameters ---------- text : str Pre-formatted text. Returns ------- str Formatted text from input. \"\"\" text = text % dict ( prog = self . _prog ) if \"%(prog)\" in text else text text = self . _whitespace_matcher . sub ( \" \" , text ) . strip () return text + \" \\n\\n \"","title":"_format_text()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._join_parts","text":"Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. Source code in torrentfile\\cli.py 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 def _join_parts ( self , part_strings ): \"\"\" Combine different sections of the help message. Parameters ---------- part_strings : list List of argument help messages and headers. Returns ------- str Fully formatted help message for CLI. \"\"\" parts = self . format_headers ( part_strings ) return super () . _join_parts ( parts )","title":"_join_parts()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._split_lines","text":"Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required Source code in torrentfile\\cli.py 94 95 96 97 98 99 100 101 102 103 104 105 106 def _split_lines ( self , text , _ ): \"\"\" Split multiline help messages and remove indentation. Parameters ---------- text : str text that needs to be split _ : int max width for line. \"\"\" lines = text . split ( \" \\n \" ) return [ line . strip () for line in lines if line ]","title":"_split_lines()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.format_headers","text":"Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. Source code in torrentfile\\cli.py 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 @staticmethod def format_headers ( parts ): \"\"\" Format help message section headers. Parameters ---------- parts : list List of individual lines for help message. Returns ------- list Input list with formatted section headers. \"\"\" if parts and parts [ 0 ] . startswith ( \"usage:\" ): parts [ 0 ] = \"Usage \\n ===== \\n \" + parts [ 0 ][ 6 :] headings = [ i for i in range ( len ( parts )) if parts [ i ] . endswith ( \": \\n \" )] for i in headings [:: - 1 ]: parts [ i ] = parts [ i ][: - 2 ] . title () underline = \"\" . join ([ \" \\n \" , \"-\" * len ( parts [ i ]), \" \\n \" ]) parts . insert ( i + 1 , underline ) return parts","title":"format_headers()"},{"location":"source/#torrentfile.cli.activate_logger","text":"Activate the builtin logging mechanism when passed debug flag from CLI. Source code in torrentfile\\cli.py 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def activate_logger (): \"\"\" Activate the builtin logging mechanism when passed debug flag from CLI. \"\"\" logging . basicConfig ( level = logging . INFO ) logger = logging . getLogger () # file_handler = logging.FileHandler( # \"torrentfile.log\", mode=\"a+\", encoding=\"utf-8\" # ) console_handler = logging . StreamHandler ( stream = sys . stderr ) # file_formatter = logging.Formatter( # \"%(asctime)s %(levelno)s %(message)s\", # datefmt=\"%m-%d %H:%M:%S\", # style=\"%\", # ) stream_formatter = logging . Formatter ( \" %(asctime)s %(levelno)s %(message)s \" , datefmt = \"%m- %d %H:%M:%S\" , style = \"%\" , ) # file_handler.setFormatter(file_formatter) console_handler . setFormatter ( stream_formatter ) # file_handler.setLevel(logging.INFO) console_handler . setLevel ( logging . DEBUG ) logger . setLevel ( logging . DEBUG ) logger . addHandler ( console_handler ) # logger.addHandler(file_handler) logger . debug ( \"Debug: ON\" )","title":"activate_logger()"},{"location":"source/#torrentfile.cli.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. Source code in torrentfile\\cli.py 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 def execute ( args = None ) -> list : \"\"\" Initialize Command Line Interface for torrentfile. Parameters ---------- args : list Commandline arguments. default=None Returns ------- list Depends on what the command line args were. \"\"\" if not args : if sys . argv [ 1 :]: args = sys . argv [ 1 :] else : args = [ \"-h\" ] parser = ArgumentParser ( \"torrentfile\" , description = ( \"Command line tools for creating, editing, checking and \" \"interacting with Bittorrent metainfo files\" ), prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , conflict_handler = \"resolve\" , ) parser . add_argument ( \"-i\" , \"--interactive\" , action = \"store_true\" , dest = \"interactive\" , help = \"select program options interactively\" , ) parser . add_argument ( \"-V\" , \"--version\" , action = \"version\" , version = f \"torrentfile v { version } \" , help = \"show program version and exit\" , ) parser . add_argument ( \"-v\" , \"--verbose\" , action = \"store_true\" , dest = \"debug\" , help = \"output debug information\" , ) parser . set_defaults ( func = parser . print_help ) subparsers = parser . add_subparsers ( title = \"Actions\" , dest = \"command\" , metavar = \"create, edit, magnet, recheck\" , ) create_parser = subparsers . add_parser ( \"create\" , help = \"\"\"Generate a new torrent meta file.\"\"\" , prefix_chars = \"-\" , aliases = [ \"c\" , \"new\" ], formatter_class = TorrentFileHelpFormatter , ) create_parser . add_argument ( \"-a\" , \"-t\" , \"--announce\" , \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , default = [], help = \"One or more space-seperated torrent tracker url(s).\" , ) create_parser . add_argument ( \"-p\" , \"--private\" , action = \"store_true\" , dest = \"private\" , help = \"Creates private torrent with multi-tracker and DHT turned off.\" , ) create_parser . add_argument ( \"-s\" , \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Add a source string. Useful for cross-seeding.\" , ) create_parser . add_argument ( \"-m\" , \"--magnet\" , action = \"store_true\" , dest = \"magnet\" , ) create_parser . add_argument ( \"-c\" , \"--comment\" , action = \"store\" , dest = \"comment\" , metavar = \"\" , help = \"Include a comment in file metadata\" , ) create_parser . add_argument ( \"-o\" , \"--out\" , action = \"store\" , dest = \"outfile\" , metavar = \"\" , help = \"Output save path for created .torrent file\" , ) create_parser . add_argument ( \"--cwd\" , \"--current\" , action = \"store_true\" , dest = \"cwd\" , help = \"Save output .torrent file to current directory\" , ) create_parser . add_argument ( \"--prog\" , \"--progress\" , default = \"1\" , action = \"store\" , dest = \"progress\" , help = \"\"\" Set the progress bar level. Options = 0, 1 (0) = Do not display progress bar. (1) = Display progress bar. \"\"\" , ) create_parser . add_argument ( \"--meta-version\" , default = \"1\" , choices = [ \"1\" , \"2\" , \"3\" ], action = \"store\" , dest = \"meta_version\" , metavar = \"\" , help = \"\"\" Bittorrent metafile version. Options = 1, 2, 3 (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid \"\"\" , ) create_parser . add_argument ( \"--piece-length\" , action = \"store\" , dest = \"piece_length\" , metavar = \"\" , help = \"\"\" (Default: ) Number of bytes for per chunk of data transmitted by Bittorrent client. Acceptable values include integers 14-26 which will be interpreted as a perfect power of 2. e.g. 14 = 16KiB pieces. Examples:: [--piece-length 14] [--piece-length 20] \"\"\" , ) create_parser . add_argument ( \"-w\" , \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"list of web addresses where torrent data exists (GetRight).\" , ) create_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"list of URLs, addresses where content can be found (Hoffman).\" , ) create_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , nargs = \"?\" , help = \"Path to content file or directory\" , ) create_parser . set_defaults ( func = create ) edit_parser = subparsers . add_parser ( \"edit\" , help = \"\"\" Edit existing torrent meta file. \"\"\" , aliases = [ \"e\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) edit_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"path to *.torrent file\" , metavar = \"<*.torrent>\" , ) edit_parser . add_argument ( \"--tracker\" , action = \"store\" , dest = \"announce\" , metavar = \"\" , nargs = \"+\" , help = \"\"\" Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). \"\"\" , ) edit_parser . add_argument ( \"--web-seed\" , action = \"store\" , dest = \"url_list\" , metavar = \"\" , nargs = \"+\" , help = \"Replace current list of web-seed urls with one or more url(s)\" , ) edit_parser . add_argument ( \"--http-seed\" , action = \"store\" , dest = \"httpseeds\" , metavar = \"\" , nargs = \"+\" , help = \"replace all currently listed addresses with new list (Hoffman).\" , ) edit_parser . add_argument ( \"--private\" , action = \"store_true\" , help = \"Make torrent private.\" , dest = \"private\" , ) edit_parser . add_argument ( \"--comment\" , help = \"Replaces any existing comment with \" , metavar = \"\" , dest = \"comment\" , action = \"store\" , ) edit_parser . add_argument ( \"--source\" , action = \"store\" , dest = \"source\" , metavar = \"\" , help = \"Replaces current source with \" , ) edit_parser . set_defaults ( func = edit ) magnet_parser = subparsers . add_parser ( \"magnet\" , help = \"\"\" Generate magnet url from an existing Bittorrent meta file. \"\"\" , aliases = [ \"m\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) magnet_parser . add_argument ( \"metafile\" , action = \"store\" , help = \"Path to Bittorrent meta file.\" , metavar = \"<*.torrent>\" , ) magnet_parser . set_defaults ( func = magnet ) check_parser = subparsers . add_parser ( \"recheck\" , help = \"\"\" Calculate amount of torrent meta file's content is found on disk. \"\"\" , aliases = [ \"r\" , \"check\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) check_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to .torrent file.\" , ) check_parser . add_argument ( \"content\" , action = \"store\" , metavar = \"\" , help = \"path to content file or directory\" , ) check_parser . set_defaults ( func = recheck ) info_parser = subparsers . add_parser ( \"info\" , help = \"\"\" Show detailed information about a torrent file. \"\"\" , aliases = [ \"i\" ], prefix_chars = \"-\" , formatter_class = TorrentFileHelpFormatter , ) info_parser . add_argument ( \"metafile\" , action = \"store\" , metavar = \"<*.torrent>\" , help = \"path to pre-existing torrent file.\" , ) info_parser . set_defaults ( func = info ) args = parser . parse_args ( args ) if args . debug : activate_logger () if args . interactive : return select_action () if hasattr ( args , \"func\" ): return args . func ( args ) return args","title":"execute()"},{"location":"source/#torrentfile.cli.main","text":"Initiate main function for CLI script. Source code in torrentfile\\cli.py 526 527 528 529 530 def main (): \"\"\" Initiate main function for CLI script. \"\"\" execute ()","title":"main()"},{"location":"source/#torrentfile.edit","text":"Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"edit"},{"location":"source/#torrentfile.edit--keywords","text":"private comment source trackers web-seeds","title":"Keywords"},{"location":"source/#torrentfile.edit.edit_torrent","text":"Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. Source code in torrentfile\\edit.py 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 def edit_torrent ( metafile : str , args : dict ) -> dict : \"\"\" Edit the properties and values in a torrent meta file. Parameters ---------- metafile : str path to the torrent meta file. args : dict key value pairs of the properties to be edited. Returns ------- dict The edited and nested Meta and info dictionaries. \"\"\" logger . debug ( \"editing torrent file %s \" , metafile ) meta = pyben . load ( metafile ) info = meta [ \"info\" ] filter_empty ( args , meta , info ) if \"comment\" in args : info [ \"comment\" ] = args [ \"comment\" ] if \"source\" in args : info [ \"source\" ] = args [ \"source\" ] if \"private\" in args : info [ \"private\" ] = 1 if \"announce\" in args : val = args . get ( \"announce\" , None ) if isinstance ( val , str ): vallist = val . split () meta [ \"announce\" ] = vallist [ 0 ] meta [ \"announce-list\" ] = [ vallist ] elif isinstance ( val , list ): meta [ \"announce\" ] = val [ 0 ] meta [ \"announce-list\" ] = [ val ] if \"url-list\" in args : val = args . get ( \"url-list\" ) if isinstance ( val , str ): meta [ \"url-list\" ] = val . split () elif isinstance ( val , list ): meta [ \"url-list\" ] = val if \"httpseeds\" in args : val = args . get ( \"httpseeds\" ) if isinstance ( val , str ): meta [ \"httpseeds\" ] = val . split () elif isinstance ( val , list ): meta [ \"httpseeds\" ] = val meta [ \"info\" ] = info os . remove ( metafile ) pyben . dump ( meta , metafile ) return meta","title":"edit_torrent()"},{"location":"source/#torrentfile.edit.filter_empty","text":"Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Source code in torrentfile\\edit.py 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def filter_empty ( args : dict , meta : dict , info : dict ): \"\"\" Remove dictionary keys with empty values. Parameters ---------- args : dict Editable metafile properties from user. meta : dict Metafile data dictionary. info : dict Metafile info dictionary. \"\"\" for key , val in list ( args . items ()): if val is None : del args [ key ] continue if val == \"\" : if key in meta : del meta [ key ] elif key in info : del info [ key ] del args [ key ] logger . debug ( \"removeing empty fields %s \" , val )","title":"filter_empty()"},{"location":"source/#torrentfile.hasher","text":"Piece/File Hashers for Bittorrent meta file contents.","title":"hasher"},{"location":"source/#torrentfile.hasher.FileHasher","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 class FileHasher ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"FileHasher"},{"location":"source/#torrentfile.hasher.FileHasher.__init__","text":"Construct Hasher class instances for each file in torrent. Source code in torrentfile\\hasher.py 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 def __init__ ( self , path : str , piece_length : int , progress : bool = True , hybrid : bool = False , ): \"\"\" Construct Hasher class instances for each file in torrent. \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None self . amount = piece_length // BLOCK_SIZE self . end = False self . current = open ( path , \"rb\" ) self . hybrid = hybrid if progress : self . progressbar = True self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" )","title":"__init__()"},{"location":"source/#torrentfile.hasher.FileHasher.__iter__","text":"Return self : needed to implement iterator implementation. Source code in torrentfile\\hasher.py 444 445 446 def __iter__ ( self ): \"\"\"Return `self`: needed to implement iterator implementation.\"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.FileHasher.__next__","text":"Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. Source code in torrentfile\\hasher.py 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 def __next__ ( self ) -> bytes : \"\"\" Calculate layer hashes for contents of file. Returns ------- bytes The layer merckle root hash. \"\"\" if self . end : self . end = False raise StopIteration plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = self . current . readinto ( block ) if not size : self . end = True break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) if self . hybrid : piece . update ( block [: size ]) if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) self . prog_update ( total ) layer_hash = merkle_root ( blocks ) self . layer_hashes . append ( layer_hash ) if self . _cb : self . _cb ( layer_hash ) if self . end : self . _calculate_root () self . prog_close () if self . hybrid : if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) piece = piece . digest () self . pieces . append ( piece ) return layer_hash , piece return layer_hash","title":"__next__()"},{"location":"source/#torrentfile.hasher.FileHasher._calculate_root","text":"Calculate the root hash for opened file. Source code in torrentfile\\hasher.py 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes ) self . current . close ()","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.FileHasher._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.Hasher","text":"Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True Source code in torrentfile\\hasher.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 class Hasher ( CbMixin , ProgMixin ): \"\"\" Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters ---------- paths : list List of files. piece_length : int Size of chuncks to split the data into. progress : int default = None \"\"\" def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ])) def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"Hasher"},{"location":"source/#torrentfile.hasher.Hasher.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\hasher.py 54 55 56 57 58 59 60 61 62 63 64 65 def __init__ ( self , paths : list , piece_length : int , progress : bool = True ): \"\"\"Generate hashes of piece length data from filelist contents.\"\"\" self . piece_length = piece_length self . paths = paths self . progress = progress self . total = sum ([ os . path . getsize ( i ) for i in self . paths ]) self . index = 0 self . current = open ( self . paths [ 0 ], \"rb\" ) if self . progress : total = os . path . getsize ( self . paths [ 0 ]) self . prog_start ( total , self . paths [ 0 ], unit = \"bytes\" ) logger . debug ( \"Hashing %s \" , str ( self . paths [ 0 ]))","title":"__init__()"},{"location":"source/#torrentfile.hasher.Hasher.__iter__","text":"Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. Source code in torrentfile\\hasher.py 67 68 69 70 71 72 73 74 75 76 def __iter__ ( self ): \"\"\" Iterate through feed pieces. Returns ------- self : iterator Iterator for leaves/hash pieces. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.hasher.Hasher.__next__","text":"Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. Source code in torrentfile\\hasher.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def __next__ ( self ) -> bytes : \"\"\" Generate piece-length pieces of data from input file list. Returns ------- bytes SHA1 hash of the piece extracted. \"\"\" while True : piece = bytearray ( self . piece_length ) size = self . current . readinto ( piece ) if size == 0 : if not self . next_file (): raise StopIteration elif size < self . piece_length : self . prog_update ( size ) return self . _handle_partial ( piece [: size ]) else : self . prog_update ( size ) return sha1 ( piece ) . digest () # nosec","title":"__next__()"},{"location":"source/#torrentfile.hasher.Hasher._handle_partial","text":"Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. Source code in torrentfile\\hasher.py 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def _handle_partial ( self , arr : bytearray ) -> bytearray : \"\"\" Define the handling partial pieces that span 2 or more files. Parameters ---------- arr : bytearray Incomplete piece containing partial data Returns ------- digest : bytearray SHA1 digest of the complete piece. \"\"\" while len ( arr ) < self . piece_length and self . next_file (): target = self . piece_length - len ( arr ) temp = bytearray ( target ) size = self . current . readinto ( temp ) arr . extend ( temp [: size ]) self . prog_update ( size ) if size == target : break return sha1 ( arr ) . digest () # nosec","title":"_handle_partial()"},{"location":"source/#torrentfile.hasher.Hasher.next_file","text":"Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. Source code in torrentfile\\hasher.py 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 def next_file ( self ) -> bool : \"\"\" Seemlessly transition to next file in file list. Returns ------- bool: True if there is a next file otherwise False. \"\"\" self . index += 1 self . prog_close () if self . index < len ( self . paths ): path = self . paths [ self . index ] logger . debug ( \"Hashing %s \" , str ( path )) self . current . close () if self . progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . current = open ( path , \"rb\" ) return True return False","title":"next_file()"},{"location":"source/#torrentfile.hasher.HasherHybrid","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True Source code in torrentfile\\hasher.py 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 class HasherHybrid ( CbMixin , ProgMixin ): \"\"\" Calculate root and piece hashes for creating hybrid torrent file. **DEPRECATED** Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters ---------- path : str path to target file. piece_length : int piece length for data chunks. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data ) def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )] def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"HasherHybrid"},{"location":"source/#torrentfile.hasher.HasherHybrid.__init__","text":"Construct Hasher class instances for each file in torrent. DEPRECATED Source code in torrentfile\\hasher.py 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Construct Hasher class instances for each file in torrent. **DEPRECATED** \"\"\" self . path = path self . piece_length = piece_length self . pieces = [] self . layer_hashes = [] self . piece_layer = None self . root = None self . padding_piece = None self . padding_file = None if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) self . amount = piece_length // BLOCK_SIZE with open ( path , \"rb\" ) as data : self . process_file ( data )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherHybrid._calculate_root","text":"Calculate the root hash for opened file. DEPRECATED Source code in torrentfile\\hasher.py 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 def _calculate_root ( self ): \"\"\" Calculate the root hash for opened file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) if len ( self . layer_hashes ) > 1 : pad_piece = merkle_root ([ bytes ( 32 ) for _ in range ( self . amount )]) pow2 = next_power_2 ( len ( self . layer_hashes )) remainder = pow2 - len ( self . layer_hashes ) self . layer_hashes += [ pad_piece for _ in range ( remainder )] self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherHybrid._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Source code in torrentfile\\hasher.py 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 def _pad_remaining ( self , block_count : int ): \"\"\" Generate Hash sized, 0 filled bytes for padding. **DEPRECATED** Parameters ---------- block_count : int current total number of blocks collected. Returns ------- padding : bytes Padding to fill remaining portion of tree. \"\"\" # when the there is only one block for file remaining = self . amount - block_count if not self . layer_hashes : power2 = next_power_2 ( block_count ) remaining = power2 - block_count self . prog_update ( HASH_SIZE * remaining ) return [ bytes ( HASH_SIZE ) for _ in range ( remaining )]","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.HasherHybrid.process_file","text":"Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required Source code in torrentfile\\hasher.py 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 def process_file ( self , data : bytearray ): \"\"\" Calculate layer hashes for contents of file. **DEPRECATED** Parameters ---------- data : BytesIO File opened in read mode. \"\"\" while True : plength = self . piece_length blocks = [] piece = sha1 () # nosec total = 0 block = bytearray ( BLOCK_SIZE ) for _ in range ( self . amount ): size = data . readinto ( block ) self . prog_update ( size ) if not size : break total += size plength -= size blocks . append ( sha256 ( block [: size ]) . digest ()) piece . update ( block [: size ]) if not blocks : break if len ( blocks ) != self . amount : padding = self . _pad_remaining ( len ( blocks )) blocks . extend ( padding ) layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) if plength > 0 : self . padding_file = { \"attr\" : \"p\" , \"length\" : plength , \"path\" : [ \".pad\" , str ( plength )], } piece . update ( bytes ( plength )) self . pieces . append ( piece . digest ()) # nosec self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.HasherV2","text":"Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True Source code in torrentfile\\hasher.py 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 class HasherV2 ( CbMixin , ProgMixin ): \"\"\" Calculate the root hash and piece layers for file contents. **DEPRECATED** Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters ---------- path : str Path to file. piece_length : int Size of layer hashes pieces. progress : int default = None \"\"\" def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd ) def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close () def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"HasherV2"},{"location":"source/#torrentfile.hasher.HasherV2.__init__","text":"Calculate and store hash information for specific file. DEPRECATED Source code in torrentfile\\hasher.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 def __init__ ( self , path : str , piece_length : int , progress : bool = True ): \"\"\" Calculate and store hash information for specific file. **DEPRECATED** \"\"\" self . path = path self . root = None self . piece_layer = None self . layer_hashes = [] self . piece_length = piece_length self . num_blocks = piece_length // BLOCK_SIZE if progress : self . prog_start ( os . path . getsize ( path ), path , unit = \"bytes\" ) with open ( self . path , \"rb\" ) as fd : self . process_file ( fd )","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherV2._calculate_root","text":"Calculate root hash for the target file. DEPRECATED Source code in torrentfile\\hasher.py 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 def _calculate_root ( self ): \"\"\" Calculate root hash for the target file. **DEPRECATED** \"\"\" self . piece_layer = b \"\" . join ( self . layer_hashes ) hashes = len ( self . layer_hashes ) if hashes > 1 : pow2 = next_power_2 ( hashes ) remainder = pow2 - hashes pad_piece = [ bytes ( HASH_SIZE ) for _ in range ( self . num_blocks )] for _ in range ( remainder ): self . layer_hashes . append ( merkle_root ( pad_piece )) self . root = merkle_root ( self . layer_hashes )","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherV2.process_file","text":"Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required Source code in torrentfile\\hasher.py 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 def process_file ( self , fd : str ): \"\"\" Calculate hashes over 16KiB chuncks of file content. **DEPRECATED** Parameters ---------- fd : TextIOWrapper Opened file in read mode. \"\"\" while True : blocks = [] leaf = bytearray ( BLOCK_SIZE ) # generate leaves of merkle tree for _ in range ( self . num_blocks ): size = fd . readinto ( leaf ) self . prog_update ( size ) if not size : break blocks . append ( sha256 ( leaf [: size ]) . digest ()) # blocks is empty mean eof if not blocks : break if len ( blocks ) != self . num_blocks : # when size of file doesn't fill the last block # when the file contains multiple pieces remaining = self . num_blocks - len ( blocks ) if not self . layer_hashes : # when the there is only one block for file power2 = next_power_2 ( len ( blocks )) remaining = power2 - len ( blocks ) # pad the the rest with zeroes to fill remaining space. padding = [ bytes ( 32 ) for _ in range ( remaining )] self . prog_update ( HASH_SIZE * remaining ) blocks . extend ( padding ) # calculate the root hash for the merkle tree up to piece-length layer_hash = merkle_root ( blocks ) if self . _cb : self . _cb ( layer_hash ) self . layer_hashes . append ( layer_hash ) self . _calculate_root () self . prog_close ()","title":"process_file()"},{"location":"source/#torrentfile.hasher.merkle_root","text":"Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Source code in torrentfile\\hasher.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 def merkle_root ( blocks : list ) -> bytes : \"\"\" Calculate the merkle root for a seq of sha256 hash digests. Parameters ---------- blocks : list a sequence of sha256 layer hashes. Returns ------- bytes the sha256 root hash of the merkle tree. \"\"\" if blocks : while len ( blocks ) > 1 : blocks = [ sha256 ( x + y ) . digest () for x , y in zip ( * [ iter ( blocks )] * 2 ) ] return blocks [ 0 ] return blocks","title":"merkle_root()"},{"location":"source/#torrentfile.interactive","text":"Module contains the procedures used for Interactive Mode.","title":"interactive"},{"location":"source/#torrentfile.interactive.InteractiveCreator","text":"Class namespace for interactive program options. Source code in torrentfile\\interactive.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 class InteractiveCreator : \"\"\" Class namespace for interactive program options. \"\"\" def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props () def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"InteractiveCreator"},{"location":"source/#torrentfile.interactive.InteractiveCreator.__init__","text":"Initialize interactive meta file creator dialog. Source code in torrentfile\\interactive.py 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def __init__ ( self ): \"\"\" Initialize interactive meta file creator dialog. \"\"\" self . kwargs = { \"announce\" : None , \"url_list\" : None , \"private\" : None , \"source\" : None , \"comment\" : None , \"piece_length\" : None , \"outfile\" : None , \"path\" : None , \"httpseeds\" : None , } self . outfile , self . meta = self . get_props ()","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveCreator.get_props","text":"Gather details for torrentfile from user. Source code in torrentfile\\interactive.py 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 def get_props ( self ): \"\"\" Gather details for torrentfile from user. \"\"\" piece_length = get_input ( \"Piece Length (empty=auto): \" , lambda x : x . isdigit () ) self . kwargs [ \"piece_length\" ] = piece_length announce = get_input ( \"Tracker list (empty): \" , lambda x : isinstance ( x , str ) ) if announce : self . kwargs [ \"announce\" ] = announce . split () url_list = get_input ( \"Web Seed {GetRight} list (empty): \" , lambda x : isinstance ( x , str ) ) httpseeds = get_input ( \"Web Seed {Hoffman} list (empty): \" , lambda x : isinstance ( x , str ) ) if url_list : self . kwargs [ \"url_list\" ] = url_list . split () if httpseeds : self . kwargs [ \"httpseeds\" ] = httpseeds . split () comment = get_input ( \"Comment (empty): \" , None ) if comment : self . kwargs [ \"comment\" ] = comment source = get_input ( \"Source (empty): \" , None ) if source : self . kwargs [ \"source\" ] = source private = get_input ( \"Private Torrent? {Y/N}: (N)\" , lambda x : x in \"yYnN\" ) if private and private . lower () == \"y\" : self . kwargs [ \"private\" ] = 1 contents = get_input ( \"Content Path: \" , os . path . exists ) self . kwargs [ \"path\" ] = contents outfile = get_input ( f \"Output Path ( { contents } .torrent): \" , lambda x : os . path . exists ( os . path . dirname ( x )), ) if outfile : self . kwargs [ \"outfile\" ] = outfile meta_version = get_input ( \"Meta Version {1,2,3}: (1)\" , lambda x : x in \"123\" ) showcenter ( f \"creating { outfile } \" ) if meta_version == \"3\" : torrent = TorrentFileHybrid ( ** self . kwargs ) elif meta_version == \"2\" : torrent = TorrentFileV2 ( ** self . kwargs ) else : torrent = TorrentFile ( ** self . kwargs ) return torrent . write ()","title":"get_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor","text":"Interactive dialog class for torrent editing. Source code in torrentfile\\interactive.py 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 class InteractiveEditor : \"\"\" Interactive dialog class for torrent editing. \"\"\" def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), } def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out ) def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"InteractiveEditor"},{"location":"source/#torrentfile.interactive.InteractiveEditor.__init__","text":"Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required Source code in torrentfile\\interactive.py 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 def __init__ ( self , metafile : str ): \"\"\" Initialize the Interactive torrent editor guide. Parameters ---------- metafile : str user input string identifying the path to a torrent meta file. \"\"\" self . metafile = metafile self . meta = pyben . load ( metafile ) self . info = self . meta [ \"info\" ] self . args = { \"url-list\" : self . meta . get ( \"url-list\" , None ), \"httpseeds\" : self . meta . get ( \"httpseeds\" , None ), \"announce\" : self . meta . get ( \"announce-list\" , None ), \"source\" : self . info . get ( \"source\" , None ), \"private\" : self . info . get ( \"private\" , None ), \"comment\" : self . info . get ( \"comment\" , None ), }","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.edit_props","text":"Loop continuosly for edits until user signals DONE. Source code in torrentfile\\interactive.py 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def edit_props ( self ): \"\"\" Loop continuosly for edits until user signals DONE. \"\"\" while True : showcenter ( \"Choose the number for a propert the needs editing.\" \"Enter DONE when all editing has been completed.\" ) props = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"tracker\" , 5 : \"web-seed\" , 6 : \"httpseeds\" , } args = { 1 : \"comment\" , 2 : \"source\" , 3 : \"private\" , 4 : \"announce\" , 5 : \"url-list\" , 6 : \"httpseeds\" , } txt = \", \" . join (( str ( k ) + \": \" + v ) for k , v in props . items ()) prop = get_input ( txt ) if prop . lower () == \"done\" : break if prop . isdigit () and 0 < int ( prop ) < 6 : key = props [ int ( prop )] key2 = args [ int ( prop )] val = self . args . get ( key2 ) showtext ( \"Enter new property value or leave empty for no value.\" ) response = get_input ( f \" { key . title () } ( { val } ): \" ) self . sanatize_response ( key2 , response ) else : showtext ( \"Invalid input: Try again.\" ) edit_torrent ( self . metafile , self . args )","title":"edit_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.sanatize_response","text":"Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required Source code in torrentfile\\interactive.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 def sanatize_response ( self , key , response ): \"\"\" Convert the input data into a form recognizable by the program. Parameters ---------- key : str name of the property and attribute being eddited. response : str User input value the property is being edited to. \"\"\" if key in [ \"announce\" , \"url-list\" , \"httpseeds\" ]: val = response . split () else : val = response self . args [ key ] = val","title":"sanatize_response()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.show_current","text":"Display the current met file information to screen. Source code in torrentfile\\interactive.py 217 218 219 220 221 222 223 224 225 226 def show_current ( self ): \"\"\" Display the current met file information to screen. \"\"\" out = \"Current properties and values: \\n \" longest = max ([ len ( label ) for label in self . args ]) + 3 for key , val in self . args . items (): txt = ( key . title () + \":\" ) . ljust ( longest ) + str ( val ) out += f \" \\t { txt } \\n \" showtext ( out )","title":"show_current()"},{"location":"source/#torrentfile.interactive._get_input","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 def _get_input ( txt : str ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. Returns ------- str The text input received from the user. \"\"\" value = input ( txt ) return value","title":"_get_input()"},{"location":"source/#torrentfile.interactive._get_input_loop","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. Source code in torrentfile\\interactive.py 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 def _get_input_loop ( txt , func ): # pragma: no cover \"\"\" Gather information needed from user. Parameters ---------- txt : str The message usually containing instructions for the user. func : function Validate/Check user input data, failure = retry, success = continue. Returns ------- str The text input received from the user. \"\"\" while True : value = input ( txt ) if func and func ( value ): return value if not func or value == \"\" : return value showtext ( f \"Invalid input { value } : try again\" )","title":"_get_input_loop()"},{"location":"source/#torrentfile.interactive.create_torrent","text":"Create new torrent file interactively. Source code in torrentfile\\interactive.py 163 164 165 166 167 168 169 170 171 172 173 174 175 176 def create_torrent (): \"\"\" Create new torrent file interactively. \"\"\" showcenter ( \"Create Torrent\" ) showtext ( \" \\n Enter values for each of the options for the torrent creator, \" \"or leave blank for program defaults. \\n Spaces are considered item \" \"seperators for options that accept a list of values. \\n Values \" \"enclosed in () indicate the default value, while {} holds all \" \"valid choices available for the option. \\n\\n \" ) creator = InteractiveCreator () return creator","title":"create_torrent()"},{"location":"source/#torrentfile.interactive.edit_action","text":"Edit the editable values of the torrent meta file. Source code in torrentfile\\interactive.py 179 180 181 182 183 184 185 186 187 def edit_action (): \"\"\" Edit the editable values of the torrent meta file. \"\"\" showcenter ( \"Edit Torrent\" ) metafile = get_input ( \"Metafile(.torrent): \" , os . path . exists ) dialog = InteractiveEditor ( metafile ) dialog . show_current () dialog . edit_props ()","title":"edit_action()"},{"location":"source/#torrentfile.interactive.get_input","text":"Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. Source code in torrentfile\\interactive.py 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 def get_input ( * args : tuple ): # pragma: no cover \"\"\" Determine appropriate input function to call. Parameters ---------- *args : tuple Arbitrary number of args to pass to next function Returns ------- str The results of the function call. \"\"\" if len ( args ) == 2 : return _get_input_loop ( * args ) return _get_input ( * args )","title":"get_input()"},{"location":"source/#torrentfile.interactive.recheck_torrent","text":"Check torrent download completed percentage. Source code in torrentfile\\interactive.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 def recheck_torrent (): \"\"\" Check torrent download completed percentage. \"\"\" showcenter ( \"Check Torrent\" ) msg = \"Enter path to torrent contents, and corresponding torrent metafile.\" showtext ( msg ) metafile = get_input ( \"Conent Path (downloads/complete/torrentname):\" , os . path . exists ) contents = get_input ( \"Metafile (*.torrent): \" , os . path . exists ) checker = Checker ( metafile , contents ) results = checker . results () showtext ( f \"Completion for { metafile } is { results } %\" ) return results","title":"recheck_torrent()"},{"location":"source/#torrentfile.interactive.select_action","text":"Operate TorrentFile program interactively through terminal. Source code in torrentfile\\interactive.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def select_action (): \"\"\" Operate TorrentFile program interactively through terminal. \"\"\" showcenter ( \"TorrentFile: Starting Interactive Mode\" ) action = get_input ( \"Enter the action you wish to perform. \\n \" \"Action ( Create (c) | Edit (e) | Recheck (r) ): \" ) action = action . lower () if \"create\" in action or action == \"c\" : return create_torrent () if \"check\" in action or action == \"r\" : return recheck_torrent () if \"edit\" in action or action == \"e\" : return edit_action () print ( \"Unable to recognize input. Please try again.\" ) # pragma: nocover return select_action () # pragma: nocover","title":"select_action()"},{"location":"source/#torrentfile.interactive.showcenter","text":"Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required Source code in torrentfile\\interactive.py 108 109 110 111 112 113 114 115 116 117 118 119 120 def showcenter ( txt : str ): \"\"\" Print text to screen in the center position of the terminal. Parameters ---------- txt : str the preformated message to send to stdout. \"\"\" termlen = shutil . get_terminal_size () . columns padding = \" \" * int ((( termlen - len ( txt )) / 2 )) string = \"\" . join ([ \" \\n \" , padding , txt , \" \\n \" ]) showtext ( string )","title":"showcenter()"},{"location":"source/#torrentfile.interactive.showtext","text":"Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Source code in torrentfile\\interactive.py 96 97 98 99 100 101 102 103 104 105 def showtext ( txt ): \"\"\" Print contents of txt to screen. Parameters ---------- txt : str text to print to terminal. \"\"\" sys . stdout . write ( txt )","title":"showtext()"},{"location":"source/#torrentfile.recheck","text":"Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole.","title":"recheck"},{"location":"source/#torrentfile.recheck.Checker","text":"Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required","title":"Checker"},{"location":"source/#torrentfile.recheck.Checker--example","text":">> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) Source code in torrentfile\\recheck.py 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 class Checker ( ProgMixin ): \"\"\" Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters ---------- metafile : str Path to \".torrent\" file. path : str Path where the content is located in filesystem. Example ------- >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) \"\"\" _hook = None def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths () @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message ) def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root ) def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], []) def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ]) def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"Example"},{"location":"source/#torrentfile.recheck.Checker.__init__","text":"Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required Source code in torrentfile\\recheck.py 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 def __init__ ( self , metafile : str , path : str ): \"\"\" Validate data against hashes contained in .torrent file. Parameters ---------- metafile : str path to .torrent file path : str path to content or contents parent directory. \"\"\" if not os . path . exists ( metafile ): raise FileNotFoundError meta = [] thread = Thread ( target = pyben . loadinto , args = ( metafile , meta )) thread . start () self . last_log = None self . log_msg ( \"Checking: %s , %s \" , metafile , path ) self . metafile = metafile self . total = 0 self . paths = [] self . fileinfo = {} thread2 = Thread ( target = waiting , args = ( \"Extracting metadata\" , meta )) if not meta : # pragma: nocover thread2 . start () thread2 . join () self . meta = meta [ 0 ] self . info = self . meta [ \"info\" ] self . name = self . info [ \"name\" ] self . piece_length = self . info [ \"piece length\" ] if \"meta version\" in self . info : if \"pieces\" in self . info : self . meta_version = 3 else : self . meta_version = 2 else : self . meta_version = 1 self . root = self . find_root ( path ) self . check_paths ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.Checker.check_paths","text":"Gather all file paths described in the torrent file. Source code in torrentfile\\recheck.py 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 def check_paths ( self ): \"\"\" Gather all file paths described in the torrent file. \"\"\" finfo = self . fileinfo if \"length\" in self . info : self . log_msg ( \" %s points to a single file\" , self . root ) self . total = self . info [ \"length\" ] self . paths . append ( str ( self . root )) finfo [ 0 ] = { \"path\" : self . root , \"length\" : self . info [ \"length\" ], } if self . meta_version > 1 : root = self . info [ \"file tree\" ][ self . name ][ \"\" ][ \"pieces root\" ] finfo [ 0 ][ \"pieces root\" ] = root return # Otherwise Content is more than 1 file. self . log_msg ( \" %s points to a directory\" , self . root ) if self . meta_version == 1 : for i , item in enumerate ( self . info [ \"files\" ]): self . total += item [ \"length\" ] base = os . path . join ( * item [ \"path\" ]) self . fileinfo [ i ] = { \"path\" : str ( self . root / base ), \"length\" : item [ \"length\" ], } self . paths . append ( str ( self . root / base )) return self . walk_file_tree ( self . info [ \"file tree\" ], [])","title":"check_paths()"},{"location":"source/#torrentfile.recheck.Checker.find_root","text":"Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content Source code in torrentfile\\recheck.py 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 def find_root ( self , path : str ) -> str : \"\"\" Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters ---------- path : str root path to torrent content Returns ------- str root path to content \"\"\" if not os . path . exists ( path ): self . log_msg ( \"Could not locate torrent content %s .\" , path ) raise FileNotFoundError ( path ) root = Path ( path ) if root . name == self . name : self . log_msg ( \"Content found: %s .\" , str ( root )) return root if self . name in os . listdir ( root ): return root / self . name self . log_msg ( \"Could not locate torrent content in: %s \" , str ( root )) raise FileNotFoundError ( root )","title":"find_root()"},{"location":"source/#torrentfile.recheck.Checker.iter_hashes","text":"Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece Source code in torrentfile\\recheck.py 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 def iter_hashes ( self ) -> tuple : \"\"\" Produce results of comparing torrent contents piece by piece. Yields ------ chunck : bytes hash of data found on disk piece : bytes hash of data when complete and correct path : str path to file being hashed size : int length of bytes hashed for piece \"\"\" matched = consumed = 0 checker = self . piece_checker () for chunk , piece , path , size in checker ( self ): consumed += size matching = 0 if chunk == piece : matching += size matched += size yield chunk , piece , path , size total_consumed = str ( int ( consumed / self . total * 100 )) percent_matched = str ( int ( matched / consumed * 100 )) self . log_msg ( \"Processed: %s%% , Matched: %s%% \" , total_consumed , percent_matched , level = logging . DEBUG , ) self . _result = ( matched / consumed ) * 100 if consumed > 0 else 0","title":"iter_hashes()"},{"location":"source/#torrentfile.recheck.Checker.log_msg","text":"Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO Source code in torrentfile\\recheck.py 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 def log_msg ( self , * args , level = logging . INFO ): \"\"\" Log message `msg` to logger and send `msg` to callback hook. Parameters ---------- *args : dict formatting args for log message level : int Log level for this message; default=`logging.INFO` \"\"\" message = args [ 0 ] if len ( args ) >= 3 : message = message % tuple ( args [ 1 :]) elif len ( args ) == 2 : message = message % args [ 1 ] # Repeat log messages should be ignored. if message != self . last_log : self . last_log = message logger . log ( level , message ) if self . _hook and level == logging . INFO : self . _hook ( message )","title":"log_msg()"},{"location":"source/#torrentfile.recheck.Checker.piece_checker","text":"Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. Source code in torrentfile\\recheck.py 126 127 128 129 130 131 132 133 134 135 136 137 def piece_checker ( self ): \"\"\" Check individual pieces of the torrent. Returns ------- HashChecker | FeedChecker Individual piece hasher. \"\"\" if self . meta_version == 1 : return FeedChecker return HashChecker","title":"piece_checker()"},{"location":"source/#torrentfile.recheck.Checker.register_callback","text":"Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required Source code in torrentfile\\recheck.py 114 115 116 117 118 119 120 121 122 123 124 @classmethod def register_callback ( cls , hook ): \"\"\" Register hooks from 3rd party programs to access generated info. Parameters ---------- hook : function callback function for the logging feature. \"\"\" cls . _hook = hook","title":"register_callback()"},{"location":"source/#torrentfile.recheck.Checker.results","text":"Generate result percentage and store for future calls. Source code in torrentfile\\recheck.py 139 140 141 142 143 144 145 146 147 148 149 150 def results ( self ): \"\"\" Generate result percentage and store for future calls. \"\"\" responses = [] for response in self . iter_hashes (): responses . append ( response ) self . log_msg ( \"Final result for %s recheck: %s \" , self . metafile , self . _result ) return self . _result","title":"results()"},{"location":"source/#torrentfile.recheck.Checker.walk_file_tree","text":"Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required Source code in torrentfile\\recheck.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 def walk_file_tree ( self , tree : dict , partials : list ): \"\"\" Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters ---------- tree : dict File Tree dict extracted from torrent file. partials : list list of intermediate pathnames. \"\"\" for key , val in tree . items (): # Empty string means the tree's leaf is value if \"\" in val : base = os . path . join ( * partials , key ) roothash = None length = val [ \"\" ][ \"length\" ] roothash = None if not length else val [ \"\" ][ \"pieces root\" ] full = str ( self . root / base ) self . fileinfo [ len ( self . paths )] = { \"path\" : full , \"length\" : length , \"pieces root\" : roothash , } self . paths . append ( full ) self . total += length else : self . walk_file_tree ( val , partials + [ key ])","title":"walk_file_tree()"},{"location":"source/#torrentfile.recheck.FeedChecker","text":"Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required Source code in torrentfile\\recheck.py 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 class FeedChecker ( ProgMixin ): \"\"\" Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters ---------- checker : object the checker class instance. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial ) def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close () def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"FeedChecker"},{"location":"source/#torrentfile.recheck.FeedChecker.__init__","text":"Generate hashes of piece length data from filelist contents. Source code in torrentfile\\recheck.py 335 336 337 338 339 340 341 342 343 344 345 346 def __init__ ( self , checker : Checker ): \"\"\" Generate hashes of piece length data from filelist contents. \"\"\" self . piece_length = checker . piece_length self . paths = checker . paths self . pieces = checker . info [ \"pieces\" ] self . fileinfo = checker . fileinfo self . piece_map = {} self . index = 0 self . piece_count = 0 self . it = None","title":"__init__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 348 349 350 351 352 353 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" self . it = self . iter_pieces () return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__next__","text":"Yield back result of comparison. Source code in torrentfile\\recheck.py 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 def __next__ ( self ): \"\"\" Yield back result of comparison. \"\"\" try : partial = next ( self . it ) except StopIteration as itererror : raise StopIteration from itererror chunck = sha1 ( partial ) . digest () # nosec start = self . piece_count * SHA1 end = start + SHA1 piece = self . pieces [ start : end ] self . piece_count += 1 path = self . paths [ self . index ] return chunck , piece , path , len ( partial )","title":"__next__()"},{"location":"source/#torrentfile.recheck.FeedChecker._gen_padding","text":"Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. Source code in torrentfile\\recheck.py 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 def _gen_padding ( self , partial : bytes , length : int , read = 0 ) -> bytes : \"\"\" Create padded pieces where file sizes do not match. Parameters ---------- partial : bytes any remaining data from last file processed. length : int size of space that needs padding read : int portion of length already padded Yields ------ bytes A piece length sized block of zeros. \"\"\" while read < length : left = self . piece_length - len ( partial ) if length - read > left : padding = bytearray ( left ) partial . extend ( padding ) yield partial read += left partial = bytearray ( 0 ) else : partial . extend ( bytearray ( length - read )) read = length yield partial","title":"_gen_padding()"},{"location":"source/#torrentfile.recheck.FeedChecker.extract","text":"Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. Source code in torrentfile\\recheck.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 def extract ( self , path : str , partial : bytearray ) -> bytearray : \"\"\" Split file paths contents into blocks of data for hash pieces. Parameters ---------- path : str path to content. partial : bytes any remaining content from last file. Returns ------- bytearray Hash digest for block of .torrent contents. \"\"\" read = 0 length = self . fileinfo [ self . index ][ \"length\" ] partial = bytearray () if len ( partial ) == self . piece_length else partial if path not in self . paths : # pragma: no cover raise MissingPathError ( path ) with open ( path , \"rb\" ) as current : while True : bitlength = self . piece_length - len ( partial ) part = bytearray ( bitlength ) amount = current . readinto ( part ) read += amount partial . extend ( part [: amount ]) if amount < bitlength : if amount > 0 and read == length : self . prog_update ( amount ) yield partial break self . prog_update ( amount ) yield partial partial = bytearray ( 0 ) if length != read : for pad in self . _gen_padding ( partial , length , read ): yield pad","title":"extract()"},{"location":"source/#torrentfile.recheck.FeedChecker.iter_pieces","text":"Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. Source code in torrentfile\\recheck.py 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 def iter_pieces ( self ): \"\"\" Iterate through, and hash pieces of torrent contents. Yields ------ piece : bytes hash digest for block of torrent data. \"\"\" partial = bytearray () for i , path in enumerate ( self . paths ): total = self . fileinfo [ i ][ \"length\" ] self . prog_start ( total , path , unit = \"bytes\" ) self . index = i if os . path . exists ( path ): for piece in self . extract ( path , partial ): self . prog_update ( len ( piece )) if ( len ( piece ) == self . piece_length ) or ( i + 1 == len ( self . paths ) ): yield piece else : partial = piece else : length = self . fileinfo [ i ][ \"length\" ] for pad in self . _gen_padding ( partial , length ): if len ( pad ) == self . piece_length : yield pad else : partial = pad self . prog_close ()","title":"iter_pieces()"},{"location":"source/#torrentfile.recheck.HashChecker","text":"Bases: ProgMixin Verify that root hashes of content files match the .torrent files. Parameters: Name Type Description Default checker Object the checker instance that maintains variables. required Source code in torrentfile\\recheck.py 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 class HashChecker ( ProgMixin ): \"\"\" Verify that root hashes of content files match the .torrent files. Parameters ---------- checker : Object the checker instance that maintains variables. \"\"\" def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"HashChecker"},{"location":"source/#torrentfile.recheck.HashChecker.Padder","text":"Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required Source code in torrentfile\\recheck.py 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 class Padder : \"\"\" Padding class to generate padding hashes wherever needed. Parameters ---------- length: int the total size of the mock file generating padding for. piece_length : int the block size that each hash represents. \"\"\" def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest () def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"Padder"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__init__","text":"Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required Source code in torrentfile\\recheck.py 530 531 532 533 534 535 536 537 538 539 540 541 542 543 def __init__ ( self , length , piece_length ): \"\"\" Construct padding class to Mock missing or incomplete files. Parameters ---------- length : int size of the file piece_length : int the piece length for each iteration. \"\"\" self . length = length self . piece_length = piece_length self . pad = sha256 ( bytearray ( piece_length )) . digest ()","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__iter__","text":"Return self to correctly implement iterator type. Source code in torrentfile\\recheck.py 545 546 547 548 549 def __iter__ ( self ): \"\"\" Return self to correctly implement iterator type. \"\"\" return self # pragma: nocover","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__next__","text":"Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 def __next__ ( self ) -> bytes : \"\"\" Iterate through seemingly endless sha256 hashes of zeros. Returns ------- tuple : returns the padding Raises ------ StopIteration \"\"\" if self . length >= self . piece_length : self . length -= self . piece_length return self . pad if self . length > 0 : pad = sha256 ( bytearray ( self . length )) . digest () self . length -= self . length return pad raise StopIteration","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.__init__","text":"Construct a HybridChecker instance. Source code in torrentfile\\recheck.py 487 488 489 490 491 492 493 494 495 496 497 def __init__ ( self , checker : Checker ): \"\"\" Construct a HybridChecker instance. \"\"\" self . checker = checker self . paths = checker . paths self . piece_length = checker . piece_length self . fileinfo = checker . fileinfo self . piece_layers = checker . meta [ \"piece layers\" ] self . current = None self . index = - 1","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.__iter__","text":"Assign iterator and return self. Source code in torrentfile\\recheck.py 499 500 501 502 503 def __iter__ ( self ): \"\"\" Assign iterator and return self. \"\"\" return self","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.__next__","text":"Provide the result of comparison. Source code in torrentfile\\recheck.py 505 506 507 508 509 510 511 512 513 514 515 516 def __next__ ( self ): \"\"\" Provide the result of comparison. \"\"\" if self . current is None : self . next_file () try : return self . process_current () except StopIteration as itererr : if self . next_file (): return self . process_current () raise StopIteration from itererr","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.advance","text":"Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size Source code in torrentfile\\recheck.py 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 def advance ( self ) -> tuple : \"\"\" Increment the number of pieces processed for the current file. Returns ------- tuple the piece and size \"\"\" start = self . count * SHA256 end = start + SHA256 piece = self . pieces [ start : end ] self . count += 1 if self . length >= self . piece_length : self . length -= self . piece_length size = self . piece_length else : size = self . length self . length -= self . length return piece , size","title":"advance()"},{"location":"source/#torrentfile.recheck.HashChecker.next_file","text":"Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found Source code in torrentfile\\recheck.py 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 def next_file ( self ) -> bool : \"\"\" Remove all references to processed files and prepare for the next. Returns ------- bool if there is a next file found \"\"\" self . index += 1 self . prog_close () if self . current is None or self . index < len ( self . paths ): self . current = self . paths [ self . index ] self . length = self . fileinfo [ self . index ][ \"length\" ] self . root_hash = self . fileinfo [ self . index ][ \"pieces root\" ] if self . length > self . piece_length : self . pieces = self . piece_layers [ self . root_hash ] else : self . pieces = self . root_hash path = self . paths [ self . index ] self . prog_start ( self . length , path , unit = \"bytes\" ) self . count = 0 if os . path . exists ( self . current ): self . hasher = FileHasher ( path , self . piece_length , progress = 0 ) else : self . hasher = self . Padder ( self . length , self . piece_length ) return True if self . index >= len ( self . paths ): del self . current del self . length del self . root_hash del self . pieces return False","title":"next_file()"},{"location":"source/#torrentfile.recheck.HashChecker.process_current","text":"Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Source code in torrentfile\\recheck.py 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 def process_current ( self ) -> tuple : \"\"\" Gather necessary information to compare to metafile details. Returns ------- tuple a tuple containing the layer, piece, current path and size Raises ------ StopIteration \"\"\" try : layer = next ( self . hasher ) piece , size = self . advance () self . prog_update ( size ) return layer , piece , self . current , size except StopIteration as err : if self . length > 0 and self . count * SHA256 < len ( self . pieces ): self . hasher = self . Padder ( self . length , self . piece_length ) piece , size = self . advance () layer = next ( self . hasher ) self . prog_update ( 0 ) return layer , piece , self . current , size raise StopIteration from err","title":"process_current()"},{"location":"source/#torrentfile.torrent","text":"Classes and procedures pertaining to the creation of torrent meta files.","title":"torrent"},{"location":"source/#torrentfile.torrent--classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#torrentfile.torrent--constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#torrentfile.torrent--bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#torrentfile.torrent--meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#torrentfile.torrent--bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#torrentfile.torrent--v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory.","title":"v1 meta-dictionary"},{"location":"source/#torrentfile.torrent.MetaFile","text":"Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None Source code in torrentfile\\torrent.py 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 class MetaFile : \"\"\" Base Class for all TorrentFile classes. Parameters ---------- path : str target path to torrent content. Default: None announce : str One or more tracker URL's. Default: None comment : str A comment. Default: None piece_length : int Size of torrent pieces. Default: None private : bool For private trackers. Default: None outfile : str target path to write .torrent file. Default: None source : str Private tracker source. Default: None progress : str level of progress bar displayed Default: \"1\" cwd : bool If True change default save location to current directory httpseeds : list one or more web addresses where torrent content can be found. url_list : list one or more web addressess where torrent content exists. content : str alias for 'path' arg. meta_version : int indicates which Bittorrent protocol to use for hashing content \"\"\" hasher = None @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func ) def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"MetaFile"},{"location":"source/#torrentfile.torrent.MetaFile.__init__","text":"Construct MetaFile superclass and assign local attributes. Source code in torrentfile\\torrent.py 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 def __init__ ( self , path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ , ): \"\"\" Construct MetaFile superclass and assign local attributes. \"\"\" self . private = private self . cwd = cwd self . outfile = outfile self . progress = int ( progress ) self . comment = comment self . source = source self . meta_version = meta_version if content : path = content if not path : if announce and len ( announce ) > 1 and os . path . exists ( announce [ - 1 ]): path = announce [ - 1 ] announce = announce [: - 1 ] elif url_list and os . path . exists ( url_list [ - 1 ]): path = url_list [ - 1 ] url_list = url_list [: - 1 ] elif httpseeds and os . path . exists ( httpseeds [ - 1 ]): path = httpseeds [ - 1 ] httpseeds = httpseeds [: - 1 ] else : raise utils . MissingPathError ( \"Path to content is required.\" ) # base path to torrent content. self . path = path logger . debug ( \"path parameter found %s \" , path ) # Format piece_length attribute. if piece_length : self . piece_length = utils . normalize_piece_length ( piece_length ) logger . debug ( \"piece length parameter found %s \" , piece_length ) else : self . piece_length = utils . path_piece_length ( self . path ) logger . debug ( \"piece length calculated %s \" , self . piece_length ) # Assign announce URL to empty string if none provided. if not announce : self . announce , self . announce_list = \"\" , [[ \"\" ]] # Most torrent clients have editting trackers as a feature. elif isinstance ( announce , str ): self . announce , self . announce_list = announce , [[ announce ]] elif isinstance ( announce , Sequence ): self . announce , self . announce_list = announce [ 0 ], [ announce ] self . meta = { \"announce\" : self . announce , \"announce-list\" : self . announce_list , \"created by\" : f \"TorrentFile:v { version } \" , \"creation date\" : int ( datetime . timestamp ( datetime . now ())), \"info\" : {}, } if comment : self . meta [ \"info\" ][ \"comment\" ] = comment logger . debug ( \"comment parameter found %s \" , comment ) if private : self . meta [ \"info\" ][ \"private\" ] = 1 logger . debug ( \"private parameter triggered\" ) if source : self . meta [ \"info\" ][ \"source\" ] = source logger . debug ( \"source parameter found %s \" , source ) if url_list : self . meta [ \"url-list\" ] = url_list logger . debug ( \"url list parameter found %s \" , str ( url_list )) if httpseeds : self . meta [ \"httpseeds\" ] = httpseeds logger . debug ( \"httpseeds parameter found %s \" , str ( httpseeds )) self . meta [ \"info\" ][ \"piece length\" ] = self . piece_length self . meta_version = meta_version parent , self . name = os . path . split ( self . path ) if not self . name : self . name = os . path . basename ( parent ) self . meta [ \"info\" ][ \"name\" ] = self . name","title":"__init__()"},{"location":"source/#torrentfile.torrent.MetaFile.assemble","text":"Overload in subclasses. Raises: Type Description Exception NotImplementedError Source code in torrentfile\\torrent.py 350 351 352 353 354 355 356 357 358 359 def assemble ( self ): \"\"\" Overload in subclasses. Raises ------ Exception NotImplementedError \"\"\" raise NotImplementedError","title":"assemble()"},{"location":"source/#torrentfile.torrent.MetaFile.set_callback","text":"Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required Source code in torrentfile\\torrent.py 240 241 242 243 244 245 246 247 248 249 250 251 @classmethod def set_callback ( cls , func ): \"\"\" Assign a callback function for the Hashing class to call for each hash. Parameters ---------- func : function The callback function which accepts a single paramter. \"\"\" if \"hasher\" in vars ( cls ) and vars ( cls )[ \"hasher\" ]: cls . hasher . set_callback ( func )","title":"set_callback()"},{"location":"source/#torrentfile.torrent.MetaFile.sort_meta","text":"Sort the info and meta dictionaries. Source code in torrentfile\\torrent.py 361 362 363 364 365 366 367 def sort_meta ( self ): \"\"\"Sort the info and meta dictionaries.\"\"\" logger . debug ( \"sorting dictionary keys\" ) meta = self . meta meta [ \"info\" ] = dict ( sorted ( list ( meta [ \"info\" ] . items ()))) meta = dict ( sorted ( list ( meta . items ()))) return meta","title":"sort_meta()"},{"location":"source/#torrentfile.torrent.MetaFile.write","text":"Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. Source code in torrentfile\\torrent.py 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 def write ( self , outfile = None ) -> tuple : \"\"\" Write meta information to .torrent file. Parameters ---------- outfile : str Destination path for .torrent file. default=None Returns ------- outfile : str Where the .torrent file was writen. meta : dict .torrent meta information. \"\"\" fallback = os . path . join ( os . getcwd (), self . name ) + \".torrent\" if not self . outfile and not outfile : if self . cwd : self . outfile = fallback else : path = str ( self . path ) . rstrip ( \" \\\\ /\" ) self . outfile = path + \".torrent\" elif outfile : self . outfile = outfile if str ( self . outfile )[ - 1 ] in \" \\\\ /\" : self . outfile = self . outfile + ( self . name + \".torrent\" ) self . meta = self . sort_meta () try : pyben . dump ( self . meta , self . outfile ) except PermissionError : self . outfile = fallback pyben . dump ( self . meta , fallback ) return self . outfile , self . meta","title":"write()"},{"location":"source/#torrentfile.torrent.TorrentAssembler","text":"Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 class TorrentAssembler ( MetaFile ): \"\"\" Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = FileHasher def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentAssembler"},{"location":"source/#torrentfile.torrent.TorrentAssembler.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 658 659 660 661 662 663 664 665 666 667 668 669 670 671 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = bytearray () self . files = [] self . hybrid = self . meta_version == \"3\" self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentAssembler._traverse","text":"Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) if self . hybrid : self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) hasher = FileHasher ( path , self . piece_length , progress = True , hybrid = self . hybrid ) layers = bytearray () for result in hasher : if self . hybrid : layer_hash , piece = result self . pieces . extend ( piece ) else : layer_hash = result layers . extend ( layer_hash ) if file_size > self . piece_length : self . piece_layers [ hasher . root ] = layers if self . hybrid and hasher . padding_file : self . files . append ( hasher . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : hasher . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentAssembler.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. Source code in torrentfile\\torrent.py 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) if self . hybrid : info [ \"files\" ] = self . files if self . hybrid : info [ \"pieces\" ] = self . pieces self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFile","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} Source code in torrentfile\\torrent.py 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 class TorrentFile ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta files. Construct *Torrentfile* class instance object. Parameters ---------- **kwargs : dict Dictionary containing torrent file options. \"\"\" hasher = Hasher def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble () def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"TorrentFile"},{"location":"source/#torrentfile.torrent.TorrentFile.__init__","text":"Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} Source code in torrentfile\\torrent.py 419 420 421 422 423 424 425 426 427 428 429 430 def __init__ ( self , ** kwargs ): \"\"\" Construct TorrentFile instance with given keyword args. Parameters ---------- **kwargs : dict dictionary of keyword args passed to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v1 torrent file\" ) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFile.assemble","text":"Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file Source code in torrentfile\\torrent.py 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 def assemble ( self ): \"\"\" Assemble components of torrent metafile. Returns ------- dict metadata dictionary for torrent file \"\"\" info = self . meta [ \"info\" ] size , filelist = utils . filelist_total ( self . path ) if os . path . isfile ( self . path ): info [ \"length\" ] = size else : info [ \"files\" ] = [ { \"length\" : os . path . getsize ( path ), \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } for path in filelist ] pieces = bytearray () feeder = Hasher ( filelist , self . piece_length , self . progress ) for piece in feeder : pieces . extend ( piece ) info [ \"pieces\" ] = pieces","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid","text":"Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} Source code in torrentfile\\torrent.py 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 class TorrentFileHybrid ( MetaFile , ProgMixin ): \"\"\" Construct the Hybrid torrent meta file with provided parameters. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent options. \"\"\" hasher = HasherHybrid def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"TorrentFileHybrid"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles. Source code in torrentfile\\torrent.py 562 563 564 565 566 567 568 569 570 571 572 573 574 def __init__ ( self , ** kwargs ): \"\"\" Create Bittorrent v1 v2 hybrid metafiles. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent Hybrid file\" ) self . name = os . path . basename ( self . path ) self . hashes = [] self . piece_layers = {} self . pieces = [] self . files = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid._traverse","text":"Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required Source code in torrentfile\\torrent.py 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 def _traverse ( self , path : str ) -> dict : \"\"\" Build meta dictionary while walking directory. **DEPRECATED** Parameters ---------- path : str Path to target file. \"\"\" if os . path . isfile ( path ): file_size = os . path . getsize ( path ) self . files . append ( { \"length\" : file_size , \"path\" : os . path . relpath ( path , self . path ) . split ( os . sep ), } ) if file_size == 0 : return { \"\" : { \"length\" : file_size }} logger . debug ( \"Hashing %s \" , str ( path )) file_hash = HasherHybrid ( path , self . piece_length , self . progress ) self . prog_update ( file_size ) if file_size > self . piece_length : self . piece_layers [ file_hash . root ] = file_hash . piece_layer self . hashes . append ( file_hash ) self . pieces . extend ( file_hash . pieces ) if file_hash . padding_file : self . files . append ( file_hash . padding_file ) return { \"\" : { \"length\" : file_size , \"pieces root\" : file_hash . root }} tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): tree [ name ] = self . _traverse ( os . path . join ( path , name )) return tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. DEPRECATED Source code in torrentfile\\torrent.py 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 def assemble ( self ): \"\"\" Assemble the parts of the torrentfile into meta dictionary. **DEPRECATED** \"\"\" info = self . meta [ \"info\" ] info [ \"meta version\" ] = 2 if os . path . isfile ( self . path ): info [ \"file tree\" ] = { self . name : self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"files\" ] = self . files info [ \"pieces\" ] = b \"\" . join ( self . pieces ) self . meta [ \"piece layers\" ] = self . piece_layers return info","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileV2","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} Source code in torrentfile\\torrent.py 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 class TorrentFileV2 ( MetaFile , ProgMixin ): \"\"\" Class for creating Bittorrent meta v2 files. **DEPRECATED** Parameters ---------- **kwargs : dict Keyword arguments for torrent file options. \"\"\" hasher = HasherV2 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble () def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"TorrentFileV2"},{"location":"source/#torrentfile.torrent.TorrentFileV2.__init__","text":"Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} Source code in torrentfile\\torrent.py 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 def __init__ ( self , ** kwargs ): \"\"\" Construct `TorrentFileV2` Class instance from given parameters. **DEPRECATED** Parameters ---------- **kwargs : dict keywword arguments to pass to superclass. \"\"\" super () . __init__ ( ** kwargs ) logger . debug ( \"Assembling bittorrent v2 torrent file\" ) self . piece_layers = {} self . hashes = [] self . total = len ( utils . get_file_list ( self . path )) self . assemble ()","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileV2._traverse","text":"Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required Source code in torrentfile\\torrent.py 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 def _traverse ( self , path : str ) -> dict : \"\"\" Walk directory tree. **DEPRECATED** Parameters ---------- path : str Path to file or directory. \"\"\" if os . path . isfile ( path ): # Calculate Size and hashes for each file. size = os . path . getsize ( path ) if size == 0 : return { \"\" : { \"length\" : size }} logger . debug ( \"Hashing %s \" , str ( path )) fhash = HasherV2 ( path , self . piece_length , self . progress ) if size > self . piece_length : self . piece_layers [ fhash . root ] = fhash . piece_layer return { \"\" : { \"length\" : size , \"pieces root\" : fhash . root }} file_tree = {} if os . path . isdir ( path ): for name in sorted ( os . listdir ( path )): file_tree [ name ] = self . _traverse ( os . path . join ( path , name )) return file_tree","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileV2.assemble","text":"Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Source code in torrentfile\\torrent.py 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 def assemble ( self ): \"\"\" Assemble then return the meta dictionary for encoding. **DEPRECATED** Returns ------- meta : dict Metainformation about the torrent. \"\"\" info = self . meta [ \"info\" ] if os . path . isfile ( self . path ): info [ \"file tree\" ] = { info [ \"name\" ]: self . _traverse ( self . path )} info [ \"length\" ] = os . path . getsize ( self . path ) self . prog_update ( info [ \"length\" ]) else : info [ \"file tree\" ] = self . _traverse ( self . path ) info [ \"meta version\" ] = 2 self . meta [ \"piece layers\" ] = self . piece_layers","title":"assemble()"},{"location":"source/#torrentfile.utils","text":"Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field.","title":"utils"},{"location":"source/#torrentfile.utils.Memo","text":"Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required Source code in torrentfile\\utils.py 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 class Memo : \"\"\" Memoice chache object. Parameters ---------- func : function The function that is being memoized. \"\"\" def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {} def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"Memo"},{"location":"source/#torrentfile.utils.Memo.__call__","text":"Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. Source code in torrentfile\\utils.py 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def __call__ ( self , path ): \"\"\" Invoke each time memo function is called. Parameters ---------- path : str The relative or absolute path being used as key in cache dict. Returns ------- Any : The results of calling the function with path. \"\"\" if path in self . cache and os . path . exists ( path ): self . counter += 1 return self . cache [ path ] result = self . func ( path ) self . cache [ path ] = result return result","title":"__call__()"},{"location":"source/#torrentfile.utils.Memo.__init__","text":"Construct for memoization. Source code in torrentfile\\utils.py 51 52 53 54 55 56 57 def __init__ ( self , func ): \"\"\" Construct for memoization. \"\"\" self . func = func self . counter = 0 self . cache = {}","title":"__init__()"},{"location":"source/#torrentfile.utils.MissingPathError","text":"Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 class MissingPathError ( Exception ): \"\"\" Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"MissingPathError"},{"location":"source/#torrentfile.utils.MissingPathError.__init__","text":"Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 93 94 95 96 97 98 99 100 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file without specifying target content. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Path arguement is missing and required { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils.PieceLengthValueError","text":"Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None Source code in torrentfile\\utils.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 class PieceLengthValueError ( Exception ): \"\"\" Piece Length parameter must equal a perfect power of 2. Parameters ---------- message : str Message for user (optional). \"\"\" def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"PieceLengthValueError"},{"location":"source/#torrentfile.utils.PieceLengthValueError.__init__","text":"Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. Source code in torrentfile\\utils.py 113 114 115 116 117 118 119 120 def __init__ ( self , message : str = None ): \"\"\" Raise when creating a meta file with incorrect piece length value. The `message` argument is a message to pass to Exception base class. \"\"\" self . message = f \"Incorrect value for piece length: { str ( message ) } \" super () . __init__ ( message )","title":"__init__()"},{"location":"source/#torrentfile.utils._filelist_total","text":"Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. Source code in torrentfile\\utils.py 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def _filelist_total ( path : str ) -> tuple : \"\"\" Search directory tree for files. Parameters ---------- path : str Path to file or directory base Returns ------- int Sum of all filesizes in filelist. list All file paths within directory tree. \"\"\" if path . is_file (): file_size = os . path . getsize ( path ) return file_size , [ str ( path )] total = 0 filelist = [] if path . is_dir (): for item in path . iterdir (): size , paths = filelist_total ( item ) total += size filelist . extend ( paths ) return total , sorted ( filelist )","title":"_filelist_total()"},{"location":"source/#torrentfile.utils.filelist_total","text":"Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. Source code in torrentfile\\utils.py 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 @Memo def filelist_total ( pathstring : str ) -> os . PathLike : \"\"\" Perform error checking and format conversion to os.PathLike. Parameters ---------- pathstring : str An existing filesystem path. Returns ------- os.PathLike Input path converted to bytes format. Raises ------ MissingPathError File could not be found. \"\"\" if os . path . exists ( pathstring ): path = Path ( pathstring ) return _filelist_total ( path ) raise MissingPathError","title":"filelist_total()"},{"location":"source/#torrentfile.utils.get_file_list","text":"Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. Source code in torrentfile\\utils.py 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 def get_file_list ( path : str ) -> list : \"\"\" Return a sorted list of file paths contained in directory. Parameters ---------- path : str target file or directory. Returns ------- list sorted list of file paths. \"\"\" _ , filelist = filelist_total ( path ) return filelist","title":"get_file_list()"},{"location":"source/#torrentfile.utils.get_piece_length","text":"Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. Source code in torrentfile\\utils.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 def get_piece_length ( size : int ) -> int : \"\"\" Calculate the ideal piece length for bittorrent data. Parameters ---------- size : int Total bits of all files incluided in .torrent file. Returns ------- int Ideal piece length. \"\"\" exp = 14 while size / ( 2 ** exp ) > 200 and exp < 25 : exp += 1 return 2 ** exp","title":"get_piece_length()"},{"location":"source/#torrentfile.utils.humanize_bytes","text":"Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. Source code in torrentfile\\utils.py 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 def humanize_bytes ( amount : int ) -> str : \"\"\" Convert integer into human readable memory sized denomination. Parameters ---------- amount : int total number of bytes. Returns ------- str human readable representation of the given amount of bytes. \"\"\" if amount < 1024 : return str ( amount ) if 1024 <= amount < 1_048_576 : return f \" { amount // 1024 } KiB\" if 1_048_576 <= amount < 1_073_741_824 : return f \" { amount // 1_048_576 } MiB\" return f \" { amount // 1073741824 } GiB\"","title":"humanize_bytes()"},{"location":"source/#torrentfile.utils.next_power_2","text":"Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. Source code in torrentfile\\utils.py 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 def next_power_2 ( value : int ) -> int : \"\"\" Calculate the next perfect power of 2 equal to or greater than value. Parameters ---------- value : int integer value that is less than some perfect power of 2. Returns ------- int The next power of 2 greater than value, or value if already power of 2. \"\"\" if not value & ( value - 1 ) and value : return value start = 1 while start < value : start <<= 1 return start","title":"next_power_2()"},{"location":"source/#torrentfile.utils.normalize_piece_length","text":"Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. Source code in torrentfile\\utils.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 def normalize_piece_length ( piece_length : int ) -> int : \"\"\" Verify input piece_length is valid and convert accordingly. Parameters ---------- piece_length : int | str The piece length provided by user. Returns ------- int normalized piece length. Raises ------ PieceLengthValueError : If piece length is improper value. \"\"\" if isinstance ( piece_length , str ): if piece_length . isnumeric (): piece_length = int ( piece_length ) else : raise PieceLengthValueError ( piece_length ) if 13 < piece_length < 26 : return 2 ** piece_length if piece_length <= 13 : raise PieceLengthValueError ( piece_length ) log = int ( math . log2 ( piece_length )) if 2 ** log == piece_length : return piece_length raise PieceLengthValueError","title":"normalize_piece_length()"},{"location":"source/#torrentfile.utils.path_piece_length","text":"Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. Source code in torrentfile\\utils.py 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 def path_piece_length ( path : str ) -> int : \"\"\" Calculate piece length for input path and contents. Parameters ---------- path : str The absolute path to directory and contents. Returns ------- int The size of pieces of torrent content. \"\"\" psize = path_size ( path ) return get_piece_length ( psize )","title":"path_piece_length()"},{"location":"source/#torrentfile.utils.path_size","text":"Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. Source code in torrentfile\\utils.py 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 def path_size ( path : str ) -> int : \"\"\" Return the total size of all files in path recursively. Parameters ---------- path : str path to target file or directory. Returns ------- int total size of files. \"\"\" total_size , _ = filelist_total ( path ) return total_size","title":"path_size()"},{"location":"source/#torrentfile.utils.path_stat","text":"Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Source code in torrentfile\\utils.py 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 def path_stat ( path : str ) -> tuple : \"\"\" Calculate directory statistics. Parameters ---------- path : str The path to start calculating from. Returns ------- list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. \"\"\" total_size , filelist = filelist_total ( path ) piece_length = get_piece_length ( total_size ) return ( filelist , total_size , piece_length )","title":"path_stat()"},{"location":"source/#torrentfile.version","text":"Holds the release version number.","title":"version"},{"location":"source/#tests","text":"Unittest package init module.","title":"tests"},{"location":"source/#tests.dir1","text":"Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 168 169 170 171 172 173 174 175 176 177 178 179 @pytest . fixture ( scope = \"package\" ) def dir1 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir () yield root rmpath ( root )","title":"dir1()"},{"location":"source/#tests.dir2","text":"Create a specific temporary structured directory. Yields: Type Description str path to root of temporary directory Source code in tests\\__init__.py 182 183 184 185 186 187 188 189 190 191 192 193 @pytest . fixture () def dir2 (): \"\"\"Create a specific temporary structured directory. Yields ------ str path to root of temporary directory \"\"\" root = tempdir ( ext = \"2\" ) yield Path ( root ) rmpath ( root )","title":"dir2()"},{"location":"source/#tests.file1","text":"Return the path to a temporary file package scope. Source code in tests\\__init__.py 241 242 243 244 245 246 247 248 @pytest . fixture ( scope = \"package\" ) def file1 (): \"\"\" Return the path to a temporary file package scope. \"\"\" path = tempfile () yield path rmpath ( path )","title":"file1()"},{"location":"source/#tests.file2","text":"Return the path to a temporary file no scope. Source code in tests\\__init__.py 297 298 299 300 301 302 303 304 @pytest . fixture () def file2 (): \"\"\" Return the path to a temporary file no scope. \"\"\" path = tempfile () yield path rmpath ( path )","title":"file2()"},{"location":"source/#tests.filemeta1","text":"Test fixture for generating metafile for all versions of torrents. Source code in tests\\__init__.py 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 @pytest . fixture ( scope = \"package\" , params = torrents ()) def filemeta1 ( file1 , request ): \"\"\" Test fixture for generating metafile for all versions of torrents. \"\"\" args = { \"path\" : file1 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file1 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile )","title":"filemeta1()"},{"location":"source/#tests.filemeta2","text":"Test fixture for generating a meta file no scope. Source code in tests\\__init__.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 @pytest . fixture ( params = torrents ()) def filemeta2 ( file2 , request ): \"\"\" Test fixture for generating a meta file no scope. \"\"\" args = { \"path\" : file2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url7\" , \"url8\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } versions = torrents () version = versions . index ( request . param ) name = str ( file2 ) + \"file\" + str ( version ) + \".torrent\" torrent = request . param ( ** args ) outfile , _ = torrent . write ( outfile = name ) yield outfile rmpath ( outfile )","title":"filemeta2()"},{"location":"source/#tests.metafile1","text":"Create a standard metafile for testing. Source code in tests\\__init__.py 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 @pytest . fixture ( scope = \"package\" , params = torrents ()) def metafile1 ( dir1 , request ): \"\"\" Create a standard metafile for testing. \"\"\" versions = torrents () args = { \"path\" : dir1 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"httpseeds\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir1 ) + str ( versions . index ( torrent_class )) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile )","title":"metafile1()"},{"location":"source/#tests.metafile2","text":"Create a standard metafile for testing. Source code in tests\\__init__.py 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 @pytest . fixture ( params = torrents ()) def metafile2 ( dir2 , request ): \"\"\" Create a standard metafile for testing. \"\"\" args = { \"path\" : dir2 , \"announce\" : [ \"url1\" , \"url4\" ], \"url_list\" : [ \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"httpseeds\" : [ \"url6\" , \"url7\" ], \"source\" : \"SomeSource\" , \"private\" : 1 , } torrent_class = request . param outfile = str ( dir2 ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile )","title":"metafile2()"},{"location":"source/#tests.rmpath","text":"Remove file or directory path. Parameters: Name Type Description Default *args list Filesystem locations for removing. () Source code in tests\\__init__.py 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 def rmpath ( * args ): \"\"\"Remove file or directory path. Parameters ---------- *args : list Filesystem locations for removing. \"\"\" for arg in args : if not os . path . exists ( arg ): continue if os . path . isdir ( arg ): try : shutil . rmtree ( arg ) except PermissionError : # pragma: nocover pass elif os . path . isfile ( arg ): try : os . remove ( arg ) except PermissionError : # pragma: nocover pass","title":"rmpath()"},{"location":"source/#tests.sizedfiles","text":"Generate variable sized meta files for testing, no scope. Source code in tests\\__init__.py 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 @pytest . fixture ( params = torrents ()) def sizedfiles ( dir2 , sizes , request ): \"\"\" Generate variable sized meta files for testing, no scope. \"\"\" versions = torrents () args = { \"content\" : dir2 , \"announce\" : [ \"url1\" , \"url2\" , \"url4\" ], \"url_list\" : [ \"url5\" , \"url6\" , \"url7\" ], \"comment\" : \"this is a comment\" , \"source\" : \"SomeSource\" , \"private\" : 1 , \"piece_length\" : sizes , } torrent_class = request . param version = str ( versions . index ( torrent_class )) outfile = str ( dir2 ) + version + str ( sizes ) + \".torrent\" torrent = torrent_class ( ** args ) outfile , _ = torrent . write ( outfile = outfile ) yield outfile rmpath ( outfile )","title":"sizedfiles()"},{"location":"source/#tests.sizes","text":"Generate powers of 2 for file creation package scope. Source code in tests\\__init__.py 159 160 161 162 163 164 165 @pytest . fixture ( scope = \"package\" , params = [ 2 ** i for i in range ( 15 , 20 )]) def sizes ( request ): \"\"\" Generate powers of 2 for file creation package scope. \"\"\" size = request . param yield size","title":"sizes()"},{"location":"source/#tests.teardown","text":"Remove all temporary directories and files. Source code in tests\\__init__.py 141 142 143 144 145 146 147 148 149 @atexit . register def teardown (): # pragma: nocover \"\"\" Remove all temporary directories and files. \"\"\" root = Path ( __file__ ) . parent / \"TESTDIR\" for path in [ root , \"./torrentfile.log\" ]: if os . path . exists ( path ): rmpath ( path )","title":"teardown()"},{"location":"source/#tests.tempdir","text":"Create temporary directory. Parameters: Name Type Description Default ext str , optional extension to file names, by default \"1\" '1' Returns: Type Description str path to common root for directory. Source code in tests\\__init__.py 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 def tempdir ( ext = \"1\" ): \"\"\"Create temporary directory. Parameters ---------- ext : str, optional extension to file names, by default \"1\" Returns ------- str path to common root for directory. \"\"\" layouts = { \"1\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.mp4\" , f \"dir { ext } /file3.mp3\" , f \"dir { ext } /file4.zip\" , f \"dir { ext } /file5.txt\" , f \"dir { ext } /subdir1/subdir2/file.7z\" , f \"dir { ext } /subdir/subdir/file4.rar\" , f \"dir { ext } /subdir/subdir/file4.r01\" , ], \"2\" : [ f \"dir { ext } /file1.png\" , f \"dir { ext } /file2.jpg\" , f \"dir { ext } /subdir/file2.mp4\" , f \"dir { ext } /subdir/file3.mp3\" , ], } paths = [] for path in layouts [ ext ]: temps = tempfile ( path = path , exp = 18 ) paths . append ( temps ) return os . path . commonpath ( paths )","title":"tempdir()"},{"location":"source/#tests.tempfile","text":"Create temporary file. Creates a temporary file for unittesting purposes.py Parameters: Name Type Description Default path str , optional relative path to temporary files, by default None None exp int , optional Exponent used to determine size of file., by default 18 18 Returns: Type Description str absolute path to file. Source code in tests\\__init__.py 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 def tempfile ( path = None , exp = 18 ): \"\"\"Create temporary file. Creates a temporary file for unittesting purposes.py Parameters ---------- path : str, optional relative path to temporary files, by default None exp : int, optional Exponent used to determine size of file., by default 18 Returns ------- str absolute path to file. \"\"\" seq = ( string . printable + string . whitespace ) . encode ( \"utf-8\" ) root = Path ( __file__ ) . parent / \"TESTDIR\" if not os . path . exists ( root ): os . mkdir ( root ) if not path : path = root / ( str ( datetime . timestamp ( datetime . now ())) + \".file\" ) parts = Path ( path ) . parts partial = root for i , part in enumerate ( parts ): partial = partial / part if i == len ( parts ) - 1 : with open ( partial , \"wb\" ) as binfile : size = 2 ** exp while size > 0 : if len ( seq ) < size : binfile . write ( seq ) size -= len ( seq ) seq += seq else : binfile . write ( seq [: size ]) size -= size else : if not os . path . exists ( partial ): os . mkdir ( partial ) return partial","title":"tempfile()"},{"location":"source/#tests.torrents","text":"Return seq of torrentfile objects. Source code in tests\\__init__.py 152 153 154 155 156 def torrents (): \"\"\" Return seq of torrentfile objects. \"\"\" return [ TorrentFile , TorrentFileV2 , TorrentFileHybrid , TorrentAssembler ]","title":"torrents()"},{"location":"source/#tests.test_cli","text":"Testing functions for the command line interface.","title":"test_cli"},{"location":"source/#tests.test_cli.folder","text":"Yield a folder object as fixture. Source code in tests\\test_cli.py 41 42 43 44 45 46 47 48 49 @pytest . fixture ( scope = \"module\" ) def folder ( dir1 ): \"\"\" Yield a folder object as fixture. \"\"\" sfolder = str ( dir1 ) torrent = sfolder + \".torrent\" yield ( sfolder , torrent ) rmpath ( torrent )","title":"folder()"},{"location":"source/#tests.test_cli.test_cli_announce","text":"Test announce cli flag. Source code in tests\\test_cli.py 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce ( folder , piece_length , version ): \"\"\" Test announce cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--tracker\" , \"https://announce.org/tracker\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"announce\" ] == \"https://announce.org/tracker\"","title":"test_cli_announce()"},{"location":"source/#tests.test_cli.test_cli_announce_list","text":"Test announce-list cli flag. Source code in tests\\test_cli.py 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_announce_list ( folder , version ): \"\"\" Test announce-list cli flag. \"\"\" folder , torrent = folder trackers = [ \"https://announce.org/tracker\" , \"https://announce.net/tracker\" , \"https://tracker.net/announce\" , ] args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , version , \"--tracker\" , ] + trackers sys . argv = args execute () meta = pyben . load ( torrent ) for url in trackers : assert url in [ j for i in meta [ \"announce-list\" ] for j in i ]","title":"test_cli_announce_list()"},{"location":"source/#tests.test_cli.test_cli_announce_path","text":"Test CLI when path is placed after the trackers flag. Source code in tests\\test_cli.py 451 452 453 454 455 456 457 458 459 460 461 462 463 @pytest . mark . parametrize ( \"flag\" , [ \"-t\" , \"-w\" , \"--announce\" , \"--web-seed\" , \"--http-seed\" ] ) def test_cli_announce_path ( dir1 , flag ): \"\"\" Test CLI when path is placed after the trackers flag. \"\"\" args = [ \"torrentfile\" , \"create\" , flag , \"https://announce1.org\" , str ( dir1 )] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_announce_path()"},{"location":"source/#tests.test_cli.test_cli_comment","text":"Test comment cli flag. Source code in tests\\test_cli.py 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_comment ( folder , piece_length , version ): \"\"\" Test comment cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--magnet\" , \"--comment\" , \"this is a comment\" , \"--progress\" , \"1\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"comment\" ] == \"this is a comment\"","title":"test_cli_comment()"},{"location":"source/#tests.test_cli.test_cli_created_by","text":"Test if created torrents recieve a created by field in meta info. Source code in tests\\test_cli.py 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_created_by ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a created by field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"TorrentFile\" in meta [ \"created by\" ]","title":"test_cli_created_by()"},{"location":"source/#tests.test_cli.test_cli_creation_date","text":"Test if torrents created get an accurate timestamp. Source code in tests\\test_cli.py 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_creation_date ( folder , piece_length , version ): \"\"\" Test if torrents created get an accurate timestamp. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) num = float ( meta [ \"creation date\" ]) date = datetime . datetime . fromtimestamp ( num ) now = datetime . datetime . now () assert date . day == now . day assert date . year == now . year assert date . month == now . month","title":"test_cli_creation_date()"},{"location":"source/#tests.test_cli.test_cli_cwd","text":"Test outfile cli flag. Source code in tests\\test_cli.py 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 def test_cli_cwd ( folder ): \"\"\" Test outfile cli flag. \"\"\" folder , _ = folder args = [ \"torrentfile\" , \"create\" , \"--cwd\" , folder , ] sys . argv = args current = os . getcwd () name = os . path . basename ( folder ) outfile = os . path . join ( current , name ) + \".torrent\" execute () assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_cwd()"},{"location":"source/#tests.test_cli.test_cli_empty_files","text":"Test creating torrent with empty files. Source code in tests\\test_cli.py 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"progress\" , [ \"0\" , \"1\" ]) def test_cli_empty_files ( dir2 , version , progress ): \"\"\" Test creating torrent with empty files. \"\"\" args = [ \"torrentfile\" , \"create\" , str ( dir2 ), \"--meta-version\" , version , \"--source\" , \"somesource\" , \"--prog\" , progress , ] sys . argv = args def walk ( root , count ): \"\"\" Traverse directory to edit files. \"\"\" if root . is_file (): with open ( root , \"wb\" ) as _ : return 1 elif root . is_dir (): for item in root . iterdir (): if count >= 2 : break count += walk ( item , count ) return count walk ( dir2 , 0 ) execute () outfile = str ( dir2 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_empty_files()"},{"location":"source/#tests.test_cli.test_cli_help","text":"Test showing help notice cli flag. Source code in tests\\test_cli.py 352 353 354 355 356 357 358 359 360 361 def test_cli_help (): \"\"\" Test showing help notice cli flag. \"\"\" args = [ \"-h\" ] sys . argv = args try : assert execute () except SystemExit : assert True","title":"test_cli_help()"},{"location":"source/#tests.test_cli.test_cli_outfile","text":"Test outfile cli flag. Source code in tests\\test_cli.py 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_outfile ( dir1 , piece_length , version ): \"\"\" Test outfile cli flag. \"\"\" outfile = dir1 + \"test.torrent\" args = [ \"torrentfile\" , \"create\" , dir1 , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-o\" , outfile , \"--prog\" , \"1\" , ] sys . argv = args execute () assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_outfile()"},{"location":"source/#tests.test_cli.test_cli_piece_length","text":"Test piece length cli flag. Source code in tests\\test_cli.py 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_piece_length ( folder , piece_length , version ): \"\"\" Test piece length cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--progress\" , \"0\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"piece length\" ] == piece_length","title":"test_cli_piece_length()"},{"location":"source/#tests.test_cli.test_cli_private","text":"Test private cli flag. Source code in tests\\test_cli.py 85 86 87 88 89 90 91 92 93 94 def test_cli_private ( folder ): \"\"\" Test private cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--private\" ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"private\" in meta [ \"info\" ]","title":"test_cli_private()"},{"location":"source/#tests.test_cli.test_cli_slash_outpath","text":"Test if output when outpath ends with a /. Source code in tests\\test_cli.py 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 @pytest . mark . parametrize ( \"sep\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_outpath ( dir1 , sep ): \"\"\" Test if output when outpath ends with a /. \"\"\" if sys . platform != \"win32\" : sep = \"/\" # pragma: nocover parent = os . path . dirname ( dir1 ) + sep args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , \"-o\" , parent , str ( dir1 ), ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_slash_outpath()"},{"location":"source/#tests.test_cli.test_cli_slash_path","text":"Test if output when path ends with a /. Source code in tests\\test_cli.py 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 @pytest . mark . parametrize ( \"ending\" , [ \"/\" , \" \\\\ \" ]) def test_cli_slash_path ( dir1 , ending ): \"\"\" Test if output when path ends with a /. \"\"\" if sys . platform != \"win32\" and ending == \" \\\\ \" : # pragma: nocover ending = \"/\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"https://announce1.org\" , \"--private\" , str ( dir1 ) + ending , ] sys . argv = args execute () outfile = str ( dir1 ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( outfile )","title":"test_cli_slash_path()"},{"location":"source/#tests.test_cli.test_cli_v1","text":"Basic create torrent cli command. Source code in tests\\test_cli.py 52 53 54 55 56 57 58 59 60 def test_cli_v1 ( folder ): \"\"\" Basic create torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_v1()"},{"location":"source/#tests.test_cli.test_cli_v2","text":"Create torrent v2 cli command. Source code in tests\\test_cli.py 63 64 65 66 67 68 69 70 71 def test_cli_v2 ( folder ): \"\"\" Create torrent v2 cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"2\" ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_v2()"},{"location":"source/#tests.test_cli.test_cli_v3","text":"Create hybrid torrent cli command. Source code in tests\\test_cli.py 74 75 76 77 78 79 80 81 82 def test_cli_v3 ( folder ): \"\"\" Create hybrid torrent cli command. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--meta-version\" , \"3\" ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_v3()"},{"location":"source/#tests.test_cli.test_cli_web_seeds","text":"Test if created torrents recieve a web seeds field in meta info. Source code in tests\\test_cli.py 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_web_seeds ( folder , piece_length , version ): \"\"\" Test if created torrents recieve a web seeds field in meta info. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"-w\" , \"https://webseed.url/1\" , \"https://webseed.url/2\" , \"https://webseed.url/3\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert \"https://webseed.url/1\" in meta [ \"url-list\" ]","title":"test_cli_web_seeds()"},{"location":"source/#tests.test_cli.test_cli_with_debug","text":"Test debug mode cli flag. Source code in tests\\test_cli.py 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_debug ( folder , piece_length , version ): \"\"\" Test debug mode cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"-v\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--comment\" , \"this is a comment\" , ] sys . argv = args execute () assert os . path . exists ( torrent )","title":"test_cli_with_debug()"},{"location":"source/#tests.test_cli.test_cli_with_source","text":"Test source cli flag. Source code in tests\\test_cli.py 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** exp for exp in range ( 14 , 21 )]) @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) def test_cli_with_source ( folder , piece_length , version ): \"\"\" Test source cli flag. \"\"\" folder , torrent = folder args = [ \"torrentfile\" , \"create\" , folder , \"--piece-length\" , str ( piece_length ), \"--meta-version\" , version , \"--source\" , \"somesource\" , ] sys . argv = args execute () meta = pyben . load ( torrent ) assert meta [ \"info\" ][ \"source\" ] == \"somesource\"","title":"test_cli_with_source()"},{"location":"source/#tests.test_cli.test_fix","text":"Test dir1 fixture is not None. Source code in tests\\test_cli.py 34 35 36 37 38 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and dir2","title":"test_fix()"},{"location":"source/#tests.test_commands","text":"Testing functions for the sub-action commands from command line args.","title":"test_commands"},{"location":"source/#tests.test_commands.test_create_unicode_name","text":"Test Unicode information in CLI args. Source code in tests\\test_commands.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 def test_create_unicode_name ( file1 ): \"\"\" Test Unicode information in CLI args. \"\"\" parent = os . path . dirname ( file1 ) filename = os . path . join ( parent , \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" ) args = [ \"torrentfile\" , \"-v\" , \"create\" , \"-a\" , \"tracker_url.com/announce_3456\" , \"tracker_url.net/announce_3456\" , \"--source\" , \"sourcetext\" , \"--comment\" , \"filename is \u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"-o\" , str ( filename ), str ( file1 ), ] sys . argv = args execute () assert os . path . exists ( filename )","title":"test_create_unicode_name()"},{"location":"source/#tests.test_commands.test_fix","text":"Test dir1 fixture is not None. Source code in tests\\test_commands.py 37 38 39 40 41 def test_fix (): \"\"\" Test dir1 fixture is not None. \"\"\" assert dir1 and metafile1 and file1 and metafile2 and dir2","title":"test_fix()"},{"location":"source/#tests.test_commands.test_info","text":"Test the info_command action from the Command Line Interface. Source code in tests\\test_commands.py 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 @pytest . mark . parametrize ( \"field\" , [ \"name\" , \"announce\" , \"source\" , \"comment\" , \"private\" , \"announce-list\" ], ) def test_info ( field , file1 ): \"\"\" Test the info_command action from the Command Line Interface. \"\"\" args = [ \"torrentfile\" , \"create\" , \"-t\" , \"url1\" , \"url2\" , \"url3\" , \"--web-seed\" , \"url4\" , \"url5\" , \"--http-seed\" , \"url6\" , \"url7\" , \"--private\" , \"--comment\" , \"ExampleComment\" , \"--source\" , \"examplesource\" , str ( file1 ), ] sys . argv = args execute () class Space : \"\"\" Stand in substitution for argparse.Namespace object. \"\"\" metafile = str ( file1 ) + \".torrent\" output = info ( Space ) assert field in output","title":"test_info()"},{"location":"source/#tests.test_commands.test_magnet","text":"Test create magnet function scheme. Source code in tests\\test_commands.py 65 66 67 68 69 70 def test_magnet ( metafile1 ): \"\"\" Test create magnet function scheme. \"\"\" magnet_link = magnet ( metafile1 ) assert magnet_link . startswith ( \"magnet\" )","title":"test_magnet()"},{"location":"source/#tests.test_commands.test_magnet_cli","text":"Test magnet creation through CLI interface. Source code in tests\\test_commands.py 136 137 138 139 140 141 142 def test_magnet_cli ( metafile1 ): \"\"\" Test magnet creation through CLI interface. \"\"\" sys . argv [ 1 :] = [ \"m\" , str ( metafile1 )] uri = execute () assert \"magnet\" in uri","title":"test_magnet_cli()"},{"location":"source/#tests.test_commands.test_magnet_empty","text":"Test create magnet function scheme. Source code in tests\\test_commands.py 84 85 86 87 88 89 90 91 def test_magnet_empty (): \"\"\" Test create magnet function scheme. \"\"\" try : magnet ( \"file_that_does_not_exist\" ) except FileNotFoundError : assert True","title":"test_magnet_empty()"},{"location":"source/#tests.test_commands.test_magnet_hex","text":"Test create magnet function digest. Source code in tests\\test_commands.py 54 55 56 57 58 59 60 61 62 def test_magnet_hex ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) info = meta [ \"info\" ] binfo = sha1 ( pyben . dumps ( info )) . hexdigest () . upper () assert binfo in magnet_link","title":"test_magnet_hex()"},{"location":"source/#tests.test_commands.test_magnet_no_announce_list","text":"Test create magnet function scheme. Source code in tests\\test_commands.py 73 74 75 76 77 78 79 80 81 def test_magnet_no_announce_list ( metafile2 ): \"\"\" Test create magnet function scheme. \"\"\" meta = pyben . load ( metafile2 ) del meta [ \"announce-list\" ] pyben . dump ( meta , metafile2 ) magnet_link = magnet ( metafile2 ) assert magnet_link . startswith ( \"magnet\" )","title":"test_magnet_no_announce_list()"},{"location":"source/#tests.test_commands.test_magnet_uri","text":"Test create magnet function digest. Source code in tests\\test_commands.py 44 45 46 47 48 49 50 51 def test_magnet_uri ( metafile1 ): \"\"\" Test create magnet function digest. \"\"\" magnet_link = magnet ( metafile1 ) meta = pyben . load ( metafile1 ) announce = meta [ \"announce\" ] assert quote_plus ( announce ) in magnet_link","title":"test_magnet_uri()"},{"location":"source/#tests.test_commands.test_merkle_root_no_blocks","text":"Test running merkle root function with 1 and 0 len lists. Source code in tests\\test_commands.py 171 172 173 174 175 176 177 178 179 @pytest . mark . parametrize ( \"blocks\" , [[], [ sha1 ( b \"1010\" ) . digest ()]]) # nosec def test_merkle_root_no_blocks ( blocks ): \"\"\" Test running merkle root function with 1 and 0 len lists. \"\"\" if blocks : assert merkle_root ( blocks ) else : assert not merkle_root ( blocks )","title":"test_merkle_root_no_blocks()"},{"location":"source/#tests.test_commands.test_mixins_progbar","text":"Test progbar mixins with small file. Source code in tests\\test_commands.py 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 @pytest . mark . parametrize ( \"torrent\" , torrents ()) def test_mixins_progbar ( torrent ): \"\"\" Test progbar mixins with small file. \"\"\" tfile = tempfile ( exp = 14 ) msg = \"1234abcd\" * 80 with open ( tfile , \"wb\" ) as temp : temp . write ( msg . encode ( \"utf-8\" )) args = { \"path\" : tfile , \"--prog\" : \"1\" , } metafile = torrent ( ** args ) output , _ = metafile . write () assert output == str ( tfile ) + \".torrent\" rmpath ( tfile )","title":"test_mixins_progbar()"},{"location":"source/#tests.test_edit","text":"Testing the edit torrent feature.","title":"test_edit"},{"location":"source/#tests.test_edit.test_edit_cli","text":"Test edit torrent with all params on cli. Source code in tests\\test_edit.py 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 @pytest . mark . parametrize ( \"comment\" , [ \"commenta\" , \"commentb\" , \"commentc\" ]) @pytest . mark . parametrize ( \"source\" , [ \"sourcea\" , \"sourceb\" , \"sourcec\" ]) @pytest . mark . parametrize ( \"announce\" , [[ \"url1\" , \"url2\" , \"url3\" ], [ \"url1\" ]]) @pytest . mark . parametrize ( \"webseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) @pytest . mark . parametrize ( \"httpseed\" , [[ \"ftp1\" ], [ \"ftpa\" , \"ftpb\" ]]) def test_edit_cli ( metafile2 , comment , source , announce , webseed , httpseed ): \"\"\" Test edit torrent with all params on cli. \"\"\" sys . argv = [ \"torrentfile\" , \"edit\" , metafile2 , \"--comment\" , comment , \"--source\" , source , \"--web-seed\" , webseed , \"--http-seed\" , httpseed , \"--tracker\" , announce , \"--private\" , ] main () meta = pyben . load ( metafile2 ) info = meta [ \"info\" ] assert comment == info . get ( \"comment\" ) assert source == info . get ( \"source\" ) assert info . get ( \"private\" ) == 1 assert meta [ \"announce-list\" ] == [[ announce ]] assert meta [ \"url-list\" ] == [ webseed ]","title":"test_edit_cli()"},{"location":"source/#tests.test_edit.test_edit_comment","text":"Test edit torrent with comment param. Source code in tests\\test_edit.py 118 119 120 121 122 123 124 125 126 127 @pytest . mark . parametrize ( \"comment\" , [ \"COMMENT\" , \"COMIT\" , \"MITCO\" ]) def test_edit_comment ( metafile2 , comment ): \"\"\" Test edit torrent with comment param. \"\"\" edits = { \"comment\" : comment } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"comment\" ] == comment","title":"test_edit_comment()"},{"location":"source/#tests.test_edit.test_edit_httpseeds","text":"Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 104 105 106 107 108 109 110 111 112 113 114 115 @pytest . mark . parametrize ( \"httpseed\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_httpseeds ( metafile2 , httpseed ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"httpseeds\" : httpseed } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseed","title":"test_edit_httpseeds()"},{"location":"source/#tests.test_edit.test_edit_httpseeds_str","text":"Test edit torrent with webseed param. Source code in tests\\test_edit.py 78 79 80 81 82 83 84 85 86 87 @pytest . mark . parametrize ( \"httpseeds\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_httpseeds_str ( metafile2 , httpseeds ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"httpseeds\" : httpseeds } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"httpseeds\" ] == httpseeds . split ()","title":"test_edit_httpseeds_str()"},{"location":"source/#tests.test_edit.test_edit_none","text":"Test edit torrent with None for all params. Source code in tests\\test_edit.py 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 def test_edit_none ( metafile2 ): \"\"\" Test edit torrent with None for all params. \"\"\" edits = { \"announce\" : None , \"url-list\" : None , \"comment\" : None , \"source\" : None , \"private\" : None , } data = pyben . load ( metafile2 ) edited = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta == edited","title":"test_edit_none()"},{"location":"source/#tests.test_edit.test_edit_private_false","text":"Test edit torrent with private param False. Source code in tests\\test_edit.py 153 154 155 156 157 158 159 160 161 def test_edit_private_false ( metafile2 ): \"\"\" Test edit torrent with private param False. \"\"\" edits = { \"private\" : \"\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert \"private\" not in data [ \"info\" ]","title":"test_edit_private_false()"},{"location":"source/#tests.test_edit.test_edit_private_true","text":"Test edit torrent with private param. Source code in tests\\test_edit.py 142 143 144 145 146 147 148 149 150 def test_edit_private_true ( metafile2 ): \"\"\" Test edit torrent with private param. \"\"\" edits = { \"private\" : \"1\" } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"private\" ] == 1","title":"test_edit_private_true()"},{"location":"source/#tests.test_edit.test_edit_removal","text":"Test edit torrent with empty for all params. Source code in tests\\test_edit.py 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_edit_removal ( metafile2 ): \"\"\" Test edit torrent with empty for all params. \"\"\" edits = { \"announce\" : \"\" , \"url-list\" : \"\" , \"httpseeds\" : \"\" , \"comment\" : \"\" , \"source\" : \"\" , \"private\" : \"\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta","title":"test_edit_removal()"},{"location":"source/#tests.test_edit.test_edit_source","text":"Test edit torrent with source param. Source code in tests\\test_edit.py 130 131 132 133 134 135 136 137 138 139 @pytest . mark . parametrize ( \"source\" , [ \"SomeSource\" , \"NoSouce\" , \"MidSource\" ]) def test_edit_source ( metafile2 , source ): \"\"\" Test edit torrent with source param. \"\"\" edits = { \"source\" : source } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"info\" ][ \"source\" ] == source","title":"test_edit_source()"},{"location":"source/#tests.test_edit.test_edit_torrent","text":"Test edit torrent with announce param. Source code in tests\\test_edit.py 40 41 42 43 44 45 46 47 48 49 50 51 @pytest . mark . parametrize ( \"announce\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_torrent ( metafile2 , announce ): \"\"\" Test edit torrent with announce param. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce ]","title":"test_edit_torrent()"},{"location":"source/#tests.test_edit.test_edit_torrent_str","text":"Test edit torrent with announce param as string. Source code in tests\\test_edit.py 54 55 56 57 58 59 60 61 62 63 @pytest . mark . parametrize ( \"announce\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_torrent_str ( metafile2 , announce ): \"\"\" Test edit torrent with announce param as string. \"\"\" edits = { \"announce\" : announce } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"announce-list\" ] == [ announce . split ()]","title":"test_edit_torrent_str()"},{"location":"source/#tests.test_edit.test_edit_urllist","text":"Test edit torrent with webseed param as string. Source code in tests\\test_edit.py 90 91 92 93 94 95 96 97 98 99 100 101 @pytest . mark . parametrize ( \"url_list\" , [[ \"urla\" ], [ \"urlb\" , \"urlc\" ], [ \"urla\" , \"urlb\" , \"urlc\" ]] ) def test_edit_urllist ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param as string. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list","title":"test_edit_urllist()"},{"location":"source/#tests.test_edit.test_edit_urllist_str","text":"Test edit torrent with webseed param. Source code in tests\\test_edit.py 66 67 68 69 70 71 72 73 74 75 @pytest . mark . parametrize ( \"url_list\" , [ \"urla\" , \"urlb urlc\" , \"urla urlb urlc\" ]) def test_edit_urllist_str ( metafile2 , url_list ): \"\"\" Test edit torrent with webseed param. \"\"\" edits = { \"url-list\" : url_list } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) assert data == meta assert data [ \"url-list\" ] == url_list . split ()","title":"test_edit_urllist_str()"},{"location":"source/#tests.test_edit.test_fix","text":"Testing dir fixtures. Source code in tests\\test_edit.py 33 34 35 36 37 def test_fix (): \"\"\" Testing dir fixtures. \"\"\" assert dir2 and metafile2 and dir1","title":"test_fix()"},{"location":"source/#tests.test_edit.test_metafile_edit_with_unicode","text":"Test if editing full unicode works as it should. Source code in tests\\test_edit.py 233 234 235 236 237 238 239 240 241 242 243 244 245 246 def test_metafile_edit_with_unicode ( metafile2 ): \"\"\" Test if editing full unicode works as it should. \"\"\" edits = { \"comment\" : \"\u4e02\u4e03\u4e07\u4e08\u4e09\u4e0e\u4e0f\u4e11\u4e12\u4e13\u4e14\u4e15\u4e16\u4e17\u4e21\u4e22\u4e23\u4e24\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f.torrent\" , \"source\" : \"\u4e02\u4e03\u4e07\u4e0f\u4e11\u4e25\u4e29\u4e2a\u4e2b\u4e2c\u4e2d\u4e2e\u4e2f\" , } data = edit_torrent ( metafile2 , edits ) meta = pyben . load ( metafile2 ) com1 = data [ \"info\" ][ \"comment\" ] com2 = meta [ \"info\" ][ \"comment\" ] msg = edits [ \"comment\" ] assert com1 == com2 == msg","title":"test_metafile_edit_with_unicode()"},{"location":"source/#tests.test_interactive","text":"Testing functions for the command line interface.","title":"test_interactive"},{"location":"source/#tests.test_interactive.test_fixtures","text":"Test the fixtures used in module. Source code in tests\\test_interactive.py 37 38 39 40 41 def test_fixtures (): \"\"\" Test the fixtures used in module. \"\"\" assert filemeta2 and file1 and file2","title":"test_fixtures()"},{"location":"source/#tests.test_interactive.test_inter_create_full","text":"Test creating torrent interactively with many parameters. Source code in tests\\test_interactive.py 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 @pytest . mark . parametrize ( \"version\" , [ \"1\" , \"2\" , \"3\" ]) @pytest . mark . parametrize ( \"piece_length\" , [ \"23\" , \"18\" , \"131072\" ]) @pytest . mark . parametrize ( \"announce\" , [ \"url1\" , \"urla urlb urlc\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Do\" , \"Ra\" , \"Me\" ]) def test_inter_create_full ( file1 , piece_length , announce , comment , source , url_list , version , monkeypatch , ): \"\"\" Test creating torrent interactively with many parameters. \"\"\" mapping = [ \"create\" , piece_length , announce , url_list , url_list , comment , source , \"Y\" , file1 , str ( file1 ) + \".torrent\" , version , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta = pyben . load ( str ( file1 ) + \".torrent\" ) assert meta [ \"info\" ][ \"source\" ] == source assert meta [ \"info\" ][ \"piece length\" ] == normalize_piece_length ( piece_length ) assert meta [ \"info\" ][ \"comment\" ] == comment assert meta [ \"url-list\" ] == url_list . split ()","title":"test_inter_create_full()"},{"location":"source/#tests.test_interactive.test_inter_edit_cli","text":"Test editing torrent interactively from CLI. Source code in tests\\test_interactive.py 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 @pytest . mark . parametrize ( \"announce\" , [ \"urla urlb urlc\" , \"urld url2\" ]) @pytest . mark . parametrize ( \"urllist\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"cmnt\" , [ \"Some Comment\" ]) @pytest . mark . parametrize ( \"srce\" , [ \"Do\" , \"Ra\" ]) def test_inter_edit_cli ( filemeta2 , announce , cmnt , srce , urllist , monkeypatch ): \"\"\" Test editing torrent interactively from CLI. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , cmnt , \"2\" , srce , \"5\" , urllist , urllist , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) sys . argv = [ \"torrentfile\" , \"-i\" ] main () meta2 = pyben . load ( filemeta2 ) assert meta2 [ \"info\" ][ \"source\" ] == srce assert meta2 [ \"info\" ][ \"comment\" ] == cmnt assert meta2 [ \"url-list\" ] == urllist . split () assert meta2 [ \"info\" ][ \"private\" ] == 1","title":"test_inter_edit_cli()"},{"location":"source/#tests.test_interactive.test_inter_edit_full","text":"Test editing torrent file interactively. Source code in tests\\test_interactive.py 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 @pytest . mark . parametrize ( \"announce\" , [ \"url1\" ]) @pytest . mark . parametrize ( \"url_list\" , [ \"ftp url2\" , \"ftp1 ftp2 ftp3\" ]) @pytest . mark . parametrize ( \"comment\" , [ \"Some Comment\" , \"No Comment\" ]) @pytest . mark . parametrize ( \"source\" , [ \"Fa\" , \"So\" , \"La\" ]) def test_inter_edit_full ( filemeta2 , announce , comment , source , url_list , monkeypatch ): \"\"\" Test editing torrent file interactively. \"\"\" seq = [ \"edit\" , filemeta2 , \"4\" , announce , \"1\" , comment , \"2\" , source , \"5\" , url_list , \"\" , \"6\" , \"Y\" , \"DONE\" , ] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () meta1 = pyben . load ( filemeta2 ) assert meta1 [ \"info\" ][ \"source\" ] == source assert meta1 [ \"info\" ][ \"comment\" ] == comment assert meta1 [ \"url-list\" ] == url_list . split () assert meta1 [ \"info\" ][ \"private\" ] == 1","title":"test_inter_edit_full()"},{"location":"source/#tests.test_interactive.test_inter_recheck","text":"Test interactive recheck function. Source code in tests\\test_interactive.py 180 181 182 183 184 185 186 187 188 189 190 191 @pytest . mark . parametrize ( \"torrentclass\" , torrents ()) def test_inter_recheck ( torrentclass , monkeypatch , file1 ): \"\"\" Test interactive recheck function. \"\"\" torrent = torrentclass ( path = file1 ) filemeta , _ = torrent . write () seq = [ \"recheck\" , filemeta , str ( file1 )] it = iter ( seq ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) result = select_action () assert result == 100","title":"test_inter_recheck()"},{"location":"source/#tests.test_interactive.test_interactive_create","text":"Test creating torrent interactively. Source code in tests\\test_interactive.py 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 def test_interactive_create ( monkeypatch , file1 ): \"\"\" Test creating torrent interactively. \"\"\" mapping = [ \"create\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , \"\" , file1 , str ( file1 ) + \".torrent\" , \"\" , ] it = iter ( mapping ) monkeypatch . setattr ( MOCK , lambda * _ : next ( it )) select_action () assert os . path . exists ( str ( file1 ) + \".torrent\" )","title":"test_interactive_create()"},{"location":"source/#tests.test_recheck","text":"Testing functions for the progress module.","title":"test_recheck"},{"location":"source/#tests.test_recheck.test_checker_callback","text":"Test Checker class with directory that points to nothing. Source code in tests\\test_recheck.py 125 126 127 128 129 130 131 def test_checker_callback ( dir1 , metafile1 ): \"\"\" Test Checker class with directory that points to nothing. \"\"\" Checker . register_callback ( lambda * x : print ( x )) checker = Checker ( metafile1 , str ( dir1 )) assert checker . results () == 100","title":"test_checker_callback()"},{"location":"source/#tests.test_recheck.test_checker_class","text":"Test Checker Class against meta files. Source code in tests\\test_recheck.py 42 43 44 45 46 47 def test_checker_class ( dir1 , metafile1 ): \"\"\" Test Checker Class against meta files. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100","title":"test_checker_class()"},{"location":"source/#tests.test_recheck.test_checker_class_allfiles","text":"Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 def test_checker_class_allfiles ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" def traverse ( path ): \"\"\" Traverse internal subdirectories. \"\"\" if path . is_file (): rmpath ( path ) elif path . is_dir (): for item in path . iterdir (): traverse ( item ) traverse ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_class_allfiles()"},{"location":"source/#tests.test_recheck.test_checker_class_allpaths","text":"Test Checker class when all files are missing from contents. Source code in tests\\test_recheck.py 211 212 213 214 215 216 217 218 def test_checker_class_allpaths ( sizedfiles , dir2 ): \"\"\" Test Checker class when all files are missing from contents. \"\"\" for item in Path ( str ( dir2 )) . iterdir (): rmpath ( item ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_class_allpaths()"},{"location":"source/#tests.test_recheck.test_checker_class_half_file","text":"Test Checker class with half size single file. Source code in tests\\test_recheck.py 221 222 223 224 225 226 227 228 229 230 231 232 def test_checker_class_half_file ( filemeta2 , file2 ): \"\"\" Test Checker class with half size single file. \"\"\" half = int ( os . path . getsize ( file2 ) / 2 ) barr = bytearray ( half ) with open ( file2 , \"rb\" ) as content : content . readinto ( barr ) with open ( file2 , \"wb\" ) as content : content . write ( barr ) checker = Checker ( filemeta2 , file2 ) assert int ( checker . results ()) != 10","title":"test_checker_class_half_file()"},{"location":"source/#tests.test_recheck.test_checker_cli_args","text":"Test exclusive Checker Mode CLI. Source code in tests\\test_recheck.py 134 135 136 137 138 139 140 def test_checker_cli_args ( dir1 , metafile1 ): \"\"\" Test exclusive Checker Mode CLI. \"\"\" sys . argv = [ \"torrentfile\" , \"check\" , str ( metafile1 ), str ( dir1 )] output = main () assert output == 100","title":"test_checker_cli_args()"},{"location":"source/#tests.test_recheck.test_checker_empty_files","text":"Test Checker when directory contains 0 length files. Source code in tests\\test_recheck.py 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 def test_checker_empty_files ( dir2 , sizedfiles ): \"\"\" Test Checker when directory contains 0 length files. \"\"\" def empty_files ( root ): \"\"\" Dump contents of files. \"\"\" if os . path . isfile ( root ): with open ( root , \"wb\" ) as _ : pass assert os . path . getsize ( root ) == 0 elif os . path . isdir ( root ): for item in os . listdir ( root ): return empty_files ( os . path . join ( root , item )) return root empty_files ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100","title":"test_checker_empty_files()"},{"location":"source/#tests.test_recheck.test_checker_first_piece","text":"Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 def test_checker_first_piece ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if path . is_file (): new = b \"Something other than what was there before.\" with open ( path , \"rb\" ) as bfile : data = bfile . read () new_len = len ( new ) content = b \"\" . join ([ new , data [ new_len :]]) with open ( path , \"wb\" ) as bdoc : bdoc . write ( content ) elif path . is_dir (): for item in path . iterdir (): change ( item ) change ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100","title":"test_checker_first_piece()"},{"location":"source/#tests.test_recheck.test_checker_first_piece_alt","text":"Test Checker Class when first piece is slightly alterred. Source code in tests\\test_recheck.py 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 def test_checker_first_piece_alt ( dir2 , sizedfiles ): \"\"\" Test Checker Class when first piece is slightly alterred. \"\"\" def change ( path ): \"\"\" Change some bytes in file. \"\"\" if os . path . isfile ( path ): with open ( path , \"rb\" ) as bfile : data = bfile . read () new = b \"some_other_bytes_to_use\" new_len = len ( new ) with open ( path , \"wb\" ) as wfile : wfile . write ( new + data [ new_len :]) elif os . path . isdir ( path ): for item in os . listdir ( path ): change ( os . path . join ( path , item )) change ( dir2 ) checker = Checker ( sizedfiles , dir2 ) assert checker . results () != 100","title":"test_checker_first_piece_alt()"},{"location":"source/#tests.test_recheck.test_checker_missing","text":"Test Checker class when files are missing from contents. Source code in tests\\test_recheck.py 179 180 181 182 183 184 185 186 187 188 def test_checker_missing ( sizedfiles , dir2 ): \"\"\" Test Checker class when files are missing from contents. \"\"\" count = 0 for fd in Path ( dir2 ) . iterdir (): if fd . is_file () and count < 2 : rmpath ( fd ) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_missing()"},{"location":"source/#tests.test_recheck.test_checker_missing_singles","text":"Test Checker class with half size single file. Source code in tests\\test_recheck.py 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 def test_checker_missing_singles ( dir2 , sizedfiles ): \"\"\" Test Checker class with half size single file. \"\"\" def walk ( root ): \"\"\" Remove first file found. \"\"\" if root . is_file (): rmpath ( root ) return True if root . is_dir (): for item in root . iterdir (): walk ( item ) return False walk ( Path ( dir2 )) checker = Checker ( sizedfiles , dir2 ) assert int ( checker . results ()) < 100","title":"test_checker_missing_singles()"},{"location":"source/#tests.test_recheck.test_checker_no_meta_file","text":"Test Checker when incorrect metafile is provided. Source code in tests\\test_recheck.py 159 160 161 162 163 164 165 166 def test_checker_no_meta_file (): \"\"\" Test Checker when incorrect metafile is provided. \"\"\" try : Checker ( \"peaches\" , \"$\" ) except FileNotFoundError : assert True","title":"test_checker_no_meta_file()"},{"location":"source/#tests.test_recheck.test_checker_parent_dir","text":"Test providing the parent directory for torrent checking feature. Source code in tests\\test_recheck.py 143 144 145 146 147 148 def test_checker_parent_dir ( dir1 , metafile1 ): \"\"\" Test providing the parent directory for torrent checking feature. \"\"\" checker = Checker ( metafile1 , os . path . dirname ( dir1 )) assert checker . results () == 100","title":"test_checker_parent_dir()"},{"location":"source/#tests.test_recheck.test_checker_result_property","text":"Test Checker class with half size single file. Source code in tests\\test_recheck.py 257 258 259 260 261 262 263 def test_checker_result_property ( dir1 , metafile1 ): \"\"\" Test Checker class with half size single file. \"\"\" checker = Checker ( metafile1 , dir1 ) result = checker . results () assert checker . results () == result","title":"test_checker_result_property()"},{"location":"source/#tests.test_recheck.test_checker_simplest","text":"Test the simplest example. Source code in tests\\test_recheck.py 266 267 268 269 270 271 def test_checker_simplest ( dir1 , metafile1 ): \"\"\" Test the simplest example. \"\"\" checker = Checker ( metafile1 , dir1 ) assert checker . results () == 100","title":"test_checker_simplest()"},{"location":"source/#tests.test_recheck.test_checker_with_file","text":"Test checker with single file torrent. Source code in tests\\test_recheck.py 151 152 153 154 155 156 def test_checker_with_file ( file1 , filemeta1 ): \"\"\" Test checker with single file torrent. \"\"\" checker = Checker ( filemeta1 , file1 ) assert checker . results () == 100","title":"test_checker_with_file()"},{"location":"source/#tests.test_recheck.test_checker_wrong_root_dir","text":"Test Checker when incorrect root directory is provided. Source code in tests\\test_recheck.py 169 170 171 172 173 174 175 176 def test_checker_wrong_root_dir ( metafile1 ): \"\"\" Test Checker when incorrect root directory is provided. \"\"\" try : Checker ( metafile1 , \"fake\" ) except FileNotFoundError : assert True","title":"test_checker_wrong_root_dir()"},{"location":"source/#tests.test_recheck.test_fixtures","text":"Test fixtures exist. Source code in tests\\test_recheck.py 33 34 35 36 37 38 39 def test_fixtures (): \"\"\" Test fixtures exist. \"\"\" assert dir1 and dir2 and file1 and file2 assert filemeta1 and filemeta2 and metafile1 assert metafile2 and sizes and sizedfiles","title":"test_fixtures()"},{"location":"source/#tests.test_recheck.test_partial_metafiles","text":"Test Checker with data that is expected to be incomplete. Source code in tests\\test_recheck.py 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 def test_partial_metafiles ( dir2 , sizedfiles ): \"\"\" Test Checker with data that is expected to be incomplete. \"\"\" def shortenfile ( path ): \"\"\" Shorten a few files for testing purposes. \"\"\" with open ( path , \"rb\" ) as bfile : data = bfile . read () with open ( path , \"wb\" ) as bfile : bfile . write ( data [: - ( 2 ** 10 )]) for item in os . listdir ( dir2 ): full = os . path . join ( dir2 , item ) if os . path . isfile ( full ): shortenfile ( full ) testdir = os . path . dirname ( dir2 ) checker = Checker ( sizedfiles , testdir ) assert checker . results () != 100","title":"test_partial_metafiles()"},{"location":"source/#tests.test_recheck.test_recheck_wrong_dir","text":"Test recheck function with directory that doesn't contain the contents. Source code in tests\\test_recheck.py 297 298 299 300 301 302 303 304 305 def test_recheck_wrong_dir ( metafile1 ): \"\"\" Test recheck function with directory that doesn't contain the contents. \"\"\" grandparent = os . path . dirname ( os . path . dirname ( metafile1 )) try : _ = Checker ( metafile1 , grandparent ) except FileNotFoundError : assert True","title":"test_recheck_wrong_dir()"},{"location":"source/#tests.test_torrent","text":"Testing functions for the torrent module.","title":"test_torrent"},{"location":"source/#tests.test_torrent.test_create_cwd_fail","text":"Test cwd argument with create command failure. Source code in tests\\test_torrent.py 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 def test_create_cwd_fail (): \"\"\"Test cwd argument with create command failure.\"\"\" class SuFile : \"\"\"A mock admin file.\"\"\" @staticmethod def __fspath__ (): raise PermissionError def __str__ ( self ): return \"SuFile\" tfile = tempfile () name = os . path . basename ( tfile ) + \".torrent\" torrent = MetaFile ( path = tfile ) sufile = SuFile () torrent . write ( outfile = sufile ) current = os . path . join ( \".\" , name ) assert os . path . exists ( current ) rmpath ( tfile , current )","title":"test_create_cwd_fail()"},{"location":"source/#tests.test_torrent.test_fixtures","text":"Test pytest fixtures. Source code in tests\\test_torrent.py 32 33 34 35 36 def test_fixtures (): \"\"\" Test pytest fixtures. \"\"\" assert dir1 and dir2","title":"test_fixtures()"},{"location":"source/#tests.test_torrent.test_mbtorrent","text":"Test torrent creation for file size larger than 10MB. Source code in tests\\test_torrent.py 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 @pytest . mark . parametrize ( \"version\" , torrents ()) @pytest . mark . parametrize ( \"progress\" , [ 0 , 1 , 2 ]) def test_mbtorrent ( version , progress ): \"\"\" Test torrent creation for file size larger than 10MB. \"\"\" tfile = tempfile ( exp = 26 ) args = { \"path\" : tfile , \"progress\" : progress , \"piece_length\" : \"14\" , } torrent = version ( ** args ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile )","title":"test_mbtorrent()"},{"location":"source/#tests.test_torrent.test_metafile_assemble","text":"Test assembling base metafile exception. Source code in tests\\test_torrent.py 50 51 52 53 54 55 56 57 58 def test_metafile_assemble ( dir1 ): \"\"\" Test assembling base metafile exception. \"\"\" metafile = MetaFile ( path = dir1 ) try : metafile . assemble () except NotImplementedError : assert True","title":"test_metafile_assemble()"},{"location":"source/#tests.test_torrent.test_torrentfile_extra","text":"Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_extra ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" def walk ( item ): \"\"\" Edit files in directory structure. \"\"\" if item . is_file (): with open ( item , \"ab\" ) as binfile : binfile . write ( bytes ( 1000 )) elif item . is_dir (): for sub in item . iterdir (): walk ( sub ) walk ( dir2 ) args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\"","title":"test_torrentfile_extra()"},{"location":"source/#tests.test_torrent.test_torrentfile_missing_path","text":"Test missing path error exception. Source code in tests\\test_torrent.py 39 40 41 42 43 44 45 46 47 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_missing_path ( version ): \"\"\" Test missing path error exception. \"\"\" try : version () except MissingPathError : assert True","title":"test_torrentfile_missing_path()"},{"location":"source/#tests.test_torrent.test_torrentfile_one_empty","text":"Test creating a torrent meta file with given directory plus extra. Source code in tests\\test_torrent.py 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_one_empty ( dir2 , version ): \"\"\" Test creating a torrent meta file with given directory plus extra. \"\"\" a = next ( os . walk ( dir2 )) if len ( a [ - 1 ]) > 0 : with open ( os . path . join ( a [ 0 ], a [ - 1 ][ 0 ]), \"w\" , encoding = \"utf-8\" ) as _ : pass args = { \"path\" : dir2 , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , } torrent = version ( ** args ) assert torrent . meta [ \"announce\" ] == \"announce\"","title":"test_torrentfile_one_empty()"},{"location":"source/#tests.test_torrent.test_torrentfile_single","text":"Test creating a torrent file from a single file contents. Source code in tests\\test_torrent.py 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 @pytest . mark . parametrize ( \"num\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single ( version , num , piece_length , capsys ): \"\"\" Test creating a torrent file from a single file contents. \"\"\" tfile = tempfile ( exp = num ) with capsys . disabled (): version . set_callback ( print ) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } trent = version ( ** args ) trent . write () assert os . path . exists ( str ( tfile ) + \".torrent\" ) rmpath ( tfile , str ( tfile ) + \".torrent\" )","title":"test_torrentfile_single()"},{"location":"source/#tests.test_torrent.test_torrentfile_single_extra","text":"Test creating a torrent file from a single file contents plus extra. Source code in tests\\test_torrent.py 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 @pytest . mark . parametrize ( \"size\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piece_length\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"version\" , torrents ()) def test_torrentfile_single_extra ( version , size , piece_length ): \"\"\" Test creating a torrent file from a single file contents plus extra. \"\"\" tfile = tempfile ( exp = size ) with open ( tfile , \"ab\" ) as binfile : binfile . write ( bytes ( str ( tfile ) . encode ( \"utf-8\" ))) args = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piece_length , } torrent = version ( ** args ) torrent . write () outfile = str ( tfile ) + \".torrent\" assert os . path . exists ( outfile ) rmpath ( tfile , outfile )","title":"test_torrentfile_single_extra()"},{"location":"source/#tests.test_torrent.test_torrentfile_single_under","text":"Test creating a torrent file from less than a single file contents. Source code in tests\\test_torrent.py 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 @pytest . mark . parametrize ( \"sze\" , list ( range ( 17 , 25 ))) @pytest . mark . parametrize ( \"piecelength\" , [ 2 ** i for i in range ( 14 , 18 )]) @pytest . mark . parametrize ( \"ver\" , torrents ()) def test_torrentfile_single_under ( ver , sze , piecelength ): \"\"\" Test creating a torrent file from less than a single file contents. \"\"\" tfile = tempfile ( exp = sze ) with open ( tfile , \"rb\" ) as binfile : data = binfile . read () with open ( tfile , \"wb\" ) as binfile : binfile . write ( data [: - ( 2 ** 9 )]) kwargs = { \"path\" : tfile , \"comment\" : \"somecomment\" , \"announce\" : \"announce\" , \"piece_length\" : piecelength , } torrent = ver ( ** kwargs ) outfile , _ = torrent . write () assert os . path . exists ( outfile ) rmpath ( tfile , outfile )","title":"test_torrentfile_single_under()"},{"location":"source/#tests.test_torrent.test_waiting_mixin","text":"Test waiting function. Source code in tests\\test_torrent.py 198 199 200 201 202 203 204 205 206 def test_waiting_mixin (): \"\"\" Test waiting function. \"\"\" msg = \"Testing message\" lst = [] timeout = 3 waiting ( msg , lst , timeout = timeout ) assert len ( lst ) == 0","title":"test_waiting_mixin()"},{"location":"source/#tests.test_utils","text":"Unittest functions for testing torrentfile utils module.","title":"test_utils"},{"location":"source/#tests.test_utils.test_filelist_total","text":"Test function for acquiring a filelist for directory. Source code in tests\\test_utils.py 118 119 120 121 122 123 def test_filelist_total ( dir1 ): \"\"\" Test function for acquiring a filelist for directory. \"\"\" total , _ = utils . filelist_total ( dir1 ) assert total == ( 2 ** 18 ) * 8","title":"test_filelist_total()"},{"location":"source/#tests.test_utils.test_filelisttotal_missing","text":"Test function filelist total with missing path. Parameters: Name Type Description Default dir2 pytest . fixture fixture containing a temporary directory required Source code in tests\\test_utils.py 225 226 227 228 229 230 231 232 233 234 235 236 237 def test_filelisttotal_missing ( dir2 ): \"\"\"Test function filelist total with missing path. Parameters ---------- dir2 : pytest.fixture fixture containing a temporary directory \"\"\" rmpath ( dir2 ) try : utils . filelist_total ( dir2 ) except utils . MissingPathError : assert True","title":"test_filelisttotal_missing()"},{"location":"source/#tests.test_utils.test_get_filelist","text":"Test function for get a list of files in a directory. Source code in tests\\test_utils.py 102 103 104 105 106 107 def test_get_filelist ( dir1 ): \"\"\" Test function for get a list of files in a directory. \"\"\" filelist = utils . get_file_list ( dir1 ) assert len ( filelist ) == 8","title":"test_get_filelist()"},{"location":"source/#tests.test_utils.test_get_path_length_max","text":"Test function for getting piece length for folders max. Source code in tests\\test_utils.py 71 72 73 74 75 def test_get_path_length_max ( dir1 ): \"\"\" Test function for getting piece length for folders max. \"\"\" assert utils . path_piece_length ( dir1 ) <= ( 2 ** 27 )","title":"test_get_path_length_max()"},{"location":"source/#tests.test_utils.test_get_path_length_min","text":"Test function for getting piece length for folders min. Source code in tests\\test_utils.py 64 65 66 67 68 def test_get_path_length_min ( dir1 ): \"\"\" Test function for getting piece length for folders min. \"\"\" assert utils . path_piece_length ( dir1 ) >= ( 2 ** 14 )","title":"test_get_path_length_min()"},{"location":"source/#tests.test_utils.test_get_path_length_mod","text":"Test function for the best piece length for provided path. Source code in tests\\test_utils.py 57 58 59 60 61 def test_get_path_length_mod ( dir1 ): \"\"\" Test function for the best piece length for provided path. \"\"\" assert utils . path_piece_length ( dir1 ) % ( 2 ** 14 ) == 0","title":"test_get_path_length_mod()"},{"location":"source/#tests.test_utils.test_get_path_size","text":"Test function for getting total size of directory. Source code in tests\\test_utils.py 110 111 112 113 114 115 def test_get_path_size ( dir1 ): \"\"\" Test function for getting total size of directory. \"\"\" pathsize = utils . path_size ( dir1 ) assert pathsize == ( 2 ** 18 ) * 8","title":"test_get_path_size()"},{"location":"source/#tests.test_utils.test_get_piece_length","text":"Test function for best piece length for given size. Source code in tests\\test_utils.py 30 31 32 33 34 35 36 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length ( size ): \"\"\" Test function for best piece length for given size. \"\"\" value = utils . get_piece_length ( size ) assert value % 1024 == 0","title":"test_get_piece_length()"},{"location":"source/#tests.test_utils.test_get_piece_length_max","text":"Test function for best piece length for given size maximum. Source code in tests\\test_utils.py 39 40 41 42 43 44 45 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_max ( size ): \"\"\" Test function for best piece length for given size maximum. \"\"\" value = utils . get_piece_length ( size ) assert value < 2 ** 27","title":"test_get_piece_length_max()"},{"location":"source/#tests.test_utils.test_get_piece_length_min","text":"Test function for best piece length for given size minimum. Source code in tests\\test_utils.py 48 49 50 51 52 53 54 @pytest . mark . parametrize ( \"size\" , [ 156634528 , 2 ** 30 , 67987 , 16384 , 8563945 ]) def test_get_piece_length_min ( size ): \"\"\" Test function for best piece length for given size minimum. \"\"\" value = utils . get_piece_length ( size ) assert value >= 2 ** 14","title":"test_get_piece_length_min()"},{"location":"source/#tests.test_utils.test_humanize_bytes","text":"Test humanize bytes function. Source code in tests\\test_utils.py 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 @pytest . mark . parametrize ( \"amount, result\" , [ ( 100 , \"100\" ), ( 1100 , \"1 KiB\" ), ( 1_100_000 , \"1 MiB\" ), ( 1_100_000_000 , \"1 GiB\" ), ( 4_400_120_000 , \"4 GiB\" ), ( 4_000_120_000 , \"3 GiB\" ), ], ) def test_humanize_bytes ( amount , result ): \"\"\" Test humanize bytes function. \"\"\" assert utils . humanize_bytes ( amount ) == result","title":"test_humanize_bytes()"},{"location":"source/#tests.test_utils.test_missing_path_error","text":"Test exception for missing path parameter. Source code in tests\\test_utils.py 137 138 139 140 141 142 143 144 145 def test_missing_path_error (): \"\"\" Test exception for missing path parameter. \"\"\" try : raise utils . MissingPathError ( \"message\" ) except utils . MissingPathError : assert True assert dir2","title":"test_missing_path_error()"},{"location":"source/#tests.test_utils.test_next_power_2","text":"Test next power of 2 function in utils module. Source code in tests\\test_utils.py 148 149 150 151 152 153 154 155 156 157 @pytest . mark . parametrize ( \"value\" , [ 5 , 32 , 18 , 225 , 16384 , 256000 ]) def test_next_power_2 ( value ): \"\"\" Test next power of 2 function in utils module. \"\"\" result = utils . next_power_2 ( value ) log = math . log2 ( result ) assert log == int ( log ) assert result % 2 == 0 assert result >= value","title":"test_next_power_2()"},{"location":"source/#tests.test_utils.test_norm_plength_errors","text":"Test function to normalize piece length errors. Parameters: Name Type Description Default amount any arguments intended to raise an exception. required Source code in tests\\test_utils.py 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 @pytest . mark . parametrize ( \"amount\" , [ \"hello\" , 11 , 0 , 100000 , 28 , \"zero\" , \"fifteen\" ] ) def test_norm_plength_errors ( amount ): \"\"\"Test function to normalize piece length errors. Parameters ---------- amount : any arguments intended to raise an exception. \"\"\" try : assert utils . normalize_piece_length ( amount ) except utils . PieceLengthValueError : assert True","title":"test_norm_plength_errors()"},{"location":"source/#tests.test_utils.test_normalize_piece_length_int","text":"Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 178 179 180 181 182 183 184 185 186 187 188 189 @pytest . mark . parametrize ( \"amount, result\" , [( i , 2 ** i ) for i in range ( 14 , 25 )]) def test_normalize_piece_length_int ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result","title":"test_normalize_piece_length_int()"},{"location":"source/#tests.test_utils.test_normalize_piece_length_str","text":"Test normalize piece length function. Parameters: Name Type Description Default amount piece length or representation required result int expected output. required Source code in tests\\test_utils.py 192 193 194 195 196 197 198 199 200 201 202 203 204 205 @pytest . mark . parametrize ( \"amount, result\" , [( str ( i ), 2 ** i ) for i in range ( 14 , 21 )] ) def test_normalize_piece_length_str ( amount , result ): \"\"\"Test normalize piece length function. Parameters ---------- amount : `str` or `int` piece length or representation result : int expected output. \"\"\" assert utils . normalize_piece_length ( amount ) == result","title":"test_normalize_piece_length_str()"},{"location":"source/#tests.test_utils.test_path_stat","text":"Test function for acquiring piece length information on folder. Source code in tests\\test_utils.py 78 79 80 81 82 83 def test_path_stat ( dir1 ): \"\"\" Test function for acquiring piece length information on folder. \"\"\" _ , _ , piece_length = utils . path_stat ( dir1 ) assert piece_length % ( 2 ** 14 ) == 0","title":"test_path_stat()"},{"location":"source/#tests.test_utils.test_path_stat_filelist_size","text":"Test function for acquiring file list information on folder. Source code in tests\\test_utils.py 94 95 96 97 98 99 def test_path_stat_filelist_size ( dir1 ): \"\"\" Test function for acquiring file list information on folder. \"\"\" filelist , _ , _ = utils . path_stat ( dir1 ) assert len ( filelist ) == 8","title":"test_path_stat_filelist_size()"},{"location":"source/#tests.test_utils.test_path_stat_size","text":"Test function for acquiring total size information on folder. Source code in tests\\test_utils.py 86 87 88 89 90 91 def test_path_stat_size ( dir1 ): \"\"\" Test function for acquiring total size information on folder. \"\"\" _ , totalsize , _ = utils . path_stat ( dir1 ) assert totalsize == ( 2 ** 18 ) * 8","title":"test_path_stat_size()"},{"location":"source/#tests.test_utils.test_piecelength_error_fixtures","text":"Test exception for uninterpretable piece length value. Source code in tests\\test_utils.py 126 127 128 129 130 131 132 133 134 def test_piecelength_error_fixtures (): \"\"\" Test exception for uninterpretable piece length value. \"\"\" try : raise utils . PieceLengthValueError ( \"message\" ) except utils . PieceLengthValueError : assert True assert dir1","title":"test_piecelength_error_fixtures()"}]} \ No newline at end of file +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"TorrentFile \ud83c\udf10 Overview A simple and convenient tool for creating, reviewing, editing, and/or validating bittorrent meta files (aka torrent files). torrentfile supports all versions of Bittorrent files, including hybrid meta files, and has full unicode support. A GUI frontend for this project can be found at https://github.com/alexpdev/TorrentfileQt \ud83d\udd0c Requirements Python 3.7+ Tested on Linux, Windows and Mac \ud83d\udcbb Install via PyPi: pip install torrentfile via Git: git clone https://github.com/alexpdev/torrentfile.git cd torrentfile pip install . Download pre-compiled binaries from the release page . \ud83d\udcda Documentation Documentation can be found here or in the docs directory. torrentfile is under active development, and is subject to significant changes in the codebase between releases. \ud83d\ude80 Usage Usage ===== torrentfile [options] [command-options] ... Create, edit, show details, and check any version Bittorrent file from command line Commands -------- create (c, new) Create a torrent meta file. edit (e) Edit existing torrent meta file. magnet (m) Create magnet url from an existing Bittorrent meta file. recheck (r, check) Calculate amount of torrent meta files content is found on disk. info (i) Show detailed information about a torrent file. Usage examples can be found in the project documentation on the examples page. \ud83d\udcdd License Distributed under Apache v2 software license. See LICENSE for more information. \ud83d\udca1 Issues & Requests & PRs If you encounter any bugs or would like to request a new feature please open a new issue. PRs and other contributions are welcome https://github.com/alexpdev/torrentfile/issues Creating Torrents Basic torrent file createion > torrentfile create /path/to/content The -t --tracker -a --announce flags add one or more urls to list of trackers. > torrentfile create /path/to/content --tracker http://tracker1.com > torrentfile create -t http://tracker2 http://tracker3 --private /path/to/content > torrentfile create --tracker http://tracker /path/to/content > torrentfile create -t http://tracker1 http://tracker2 /path/to/content the --private flag indicates use by a private tracker the --source flag can be used to help with cross-seeding > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 The options for controlling the progress bar using --prog or --progress : 0 : show no progress bar at all 1 : show progress bar (default) > torrentfile -t http://tracker.com --progress 2 /path/to/content > torrentfile --prog 0 /path/to/content to specify the save location use the -o or --out flags if the path points to directory the name of torrent is autofilled. > torrentfile create -o /specific/path/name.torrent ./content to create files using bittorrent v2 use --meta-version 2 likewise --meta-version 3 creates a hybrid torrent file. > torrentfile create --meta-version 2 /path/to/content > torrentfile create --meta-version 3 /path/to/content Check/Recheck Torrent recheck torrent file /path/to/some.torrent with /path/to/content > torrentfile recheck /path/to/some.torrent /path/to/content Edit Torrent edit a torrent file > torrentfile edit [options] Create Magnet To create a magnet URI for a pre-existing torrent meta file, use the sub-command magnet or m with the path to the torrent file. > torrentfile magnet /path/to/some.torrent Interactive Mode (expiremental) Alternatively to supplying a bunch of command line arguments, interactive mode allows users to specify program options one at a time from a series of prompts. to activate interactive mode use -i or --interactive flag > torrentfile -i GUI If you prefer a windowed gui please check out the official GUI frontend here","title":"Home"},{"location":"#torrentfile","text":"","title":"TorrentFile"},{"location":"#overview","text":"A simple and convenient tool for creating, reviewing, editing, and/or validating bittorrent meta files (aka torrent files). torrentfile supports all versions of Bittorrent files, including hybrid meta files, and has full unicode support. A GUI frontend for this project can be found at https://github.com/alexpdev/TorrentfileQt","title":"\ud83c\udf10 Overview"},{"location":"#requirements","text":"Python 3.7+ Tested on Linux, Windows and Mac","title":"\ud83d\udd0c Requirements"},{"location":"#install","text":"via PyPi: pip install torrentfile via Git: git clone https://github.com/alexpdev/torrentfile.git cd torrentfile pip install . Download pre-compiled binaries from the release page .","title":"\ud83d\udcbb Install"},{"location":"#documentation","text":"Documentation can be found here or in the docs directory. torrentfile is under active development, and is subject to significant changes in the codebase between releases.","title":"\ud83d\udcda Documentation"},{"location":"#usage","text":"Usage ===== torrentfile [options] [command-options] ... Create, edit, show details, and check any version Bittorrent file from command line Commands -------- create (c, new) Create a torrent meta file. edit (e) Edit existing torrent meta file. magnet (m) Create magnet url from an existing Bittorrent meta file. recheck (r, check) Calculate amount of torrent meta files content is found on disk. info (i) Show detailed information about a torrent file. Usage examples can be found in the project documentation on the examples page.","title":"\ud83d\ude80 Usage"},{"location":"#license","text":"Distributed under Apache v2 software license. See LICENSE for more information.","title":"\ud83d\udcdd License"},{"location":"#issues-requests-prs","text":"If you encounter any bugs or would like to request a new feature please open a new issue. PRs and other contributions are welcome https://github.com/alexpdev/torrentfile/issues","title":"\ud83d\udca1 Issues & Requests & PRs"},{"location":"#creating-torrents","text":"Basic torrent file createion > torrentfile create /path/to/content The -t --tracker -a --announce flags add one or more urls to list of trackers. > torrentfile create /path/to/content --tracker http://tracker1.com > torrentfile create -t http://tracker2 http://tracker3 --private /path/to/content > torrentfile create --tracker http://tracker /path/to/content > torrentfile create -t http://tracker1 http://tracker2 /path/to/content the --private flag indicates use by a private tracker the --source flag can be used to help with cross-seeding > torrentfile create --private --source EXAMPLE --tracker https://url1 https://url2 The options for controlling the progress bar using --prog or --progress : 0 : show no progress bar at all 1 : show progress bar (default) > torrentfile -t http://tracker.com --progress 2 /path/to/content > torrentfile --prog 0 /path/to/content to specify the save location use the -o or --out flags if the path points to directory the name of torrent is autofilled. > torrentfile create -o /specific/path/name.torrent ./content to create files using bittorrent v2 use --meta-version 2 likewise --meta-version 3 creates a hybrid torrent file. > torrentfile create --meta-version 2 /path/to/content > torrentfile create --meta-version 3 /path/to/content","title":"Creating Torrents"},{"location":"#checkrecheck-torrent","text":"recheck torrent file /path/to/some.torrent with /path/to/content > torrentfile recheck /path/to/some.torrent /path/to/content","title":"Check/Recheck Torrent"},{"location":"#edit-torrent","text":"edit a torrent file > torrentfile edit [options] ","title":"Edit Torrent"},{"location":"#create-magnet","text":"To create a magnet URI for a pre-existing torrent meta file, use the sub-command magnet or m with the path to the torrent file. > torrentfile magnet /path/to/some.torrent","title":"Create Magnet"},{"location":"#interactive-mode-expiremental","text":"Alternatively to supplying a bunch of command line arguments, interactive mode allows users to specify program options one at a time from a series of prompts. to activate interactive mode use -i or --interactive flag > torrentfile -i","title":"Interactive Mode (expiremental)"},{"location":"#gui","text":"If you prefer a windowed gui please check out the official GUI frontend here","title":"GUI"},{"location":"Apache2/","text":"Apache License Version 2.0, January 2004 < http://www.apache.org/licenses/ > Terms and Conditions for use, reproduction, and distribution 1. Definitions \u201cLicense\u201d shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. \u201cLicensor\u201d shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. \u201cLegal Entity\u201d shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \u201ccontrol\u201d means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. \u201cYou\u201d (or \u201cYour\u201d) shall mean an individual or Legal Entity exercising permissions granted by this License. \u201cSource\u201d form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. \u201cObject\u201d form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. \u201cWork\u201d shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). \u201cDerivative Works\u201d shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. \u201cContribution\u201d shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \u201csubmitted\u201d means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \u201cNot a Contribution.\u201d \u201cContributor\u201d shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a \u201cNOTICE\u201d text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \u201cAS IS\u201d BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets [] replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \u201cprinted page\u201d as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"License"},{"location":"Apache2/#apache-license","text":"Version 2.0, January 2004 < http://www.apache.org/licenses/ >","title":"Apache License"},{"location":"Apache2/#terms-and-conditions-for-use-reproduction-and-distribution","text":"","title":"Terms and Conditions for use, reproduction, and distribution"},{"location":"Apache2/#1-definitions","text":"\u201cLicense\u201d shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. \u201cLicensor\u201d shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. \u201cLegal Entity\u201d shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, \u201ccontrol\u201d means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. \u201cYou\u201d (or \u201cYour\u201d) shall mean an individual or Legal Entity exercising permissions granted by this License. \u201cSource\u201d form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. \u201cObject\u201d form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. \u201cWork\u201d shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). \u201cDerivative Works\u201d shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. \u201cContribution\u201d shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, \u201csubmitted\u201d means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as \u201cNot a Contribution.\u201d \u201cContributor\u201d shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.","title":"1. Definitions"},{"location":"Apache2/#2-grant-of-copyright-license","text":"Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.","title":"2. Grant of Copyright License"},{"location":"Apache2/#3-grant-of-patent-license","text":"Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.","title":"3. Grant of Patent License"},{"location":"Apache2/#4-redistribution","text":"You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a \u201cNOTICE\u201d text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.","title":"4. Redistribution"},{"location":"Apache2/#5-submission-of-contributions","text":"Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.","title":"5. Submission of Contributions"},{"location":"Apache2/#6-trademarks","text":"This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.","title":"6. Trademarks"},{"location":"Apache2/#7-disclaimer-of-warranty","text":"Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an \u201cAS IS\u201d BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.","title":"7. Disclaimer of Warranty"},{"location":"Apache2/#8-limitation-of-liability","text":"In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.","title":"8. Limitation of Liability"},{"location":"Apache2/#9-accepting-warranty-or-additional-liability","text":"While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS","title":"9. Accepting Warranty or Additional Liability"},{"location":"Apache2/#appendix-how-to-apply-the-apache-license-to-your-work","text":"To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets [] replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same \u201cprinted page\u201d as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"APPENDIX: How to apply the Apache License to your work"},{"location":"Commands/","text":"TorrentFile CLI Menu Help Messages Main Usage ===== torrentfile [-h] [-i] [-V] [-v] ... CLI Tool for creating, checking, editing... Bittorrent meta files. TorrentFile supports all versions of torrent files. Options ------- -h, --help show this help message and exit -i, --interactive select program options interactively -V, --version show program version and exit -v, --verbose output debug information Actions ------- c (create, new) Create a torrent meta file. e (edit) Edit existing torrent meta file. m (magnet) Create magnet url from an existing Bittorrent meta file. r (recheck, check) Calculate amount of torrent meta file's content is found on disk. i (info) Show detailed information about a torrent file. Create Usage ===== torrentfile c [-h] [-a [ ...]] [-p] [-s ] [-m] [-c ] [-o ] [-t [ ...]] [--noprogress] [--meta-version ] [--piece-length ] [-w [ ...]] Positional Arguments -------------------- Path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit -a [ ...], --announce [ ...] Alias for -t/--tracker -p, --private Create a private torrent file -s , --source Useful for cross-seeding -m, --magnet Output Magnet Link after creation completes -c , --comment Include a comment in file metadata -o , --out Output path for created .torrent file -t [ ...], --tracker [ ...] One or more Bittorrent tracker announce url(s). --prog, --progress (0) = no progress bar displayed (1) = progress bar is displayed (default) --meta-version Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid --piece-length Number of bytes per piece. (Default: None) Acceptable inputs include {14 - 24} as exponent for 2^n, or any acceptable integer value (must be power of 2). Examples:: [--piece-length 14] [--piece-length 16777216] -w [ ...], --web-seed [ ...] One or more url(s) linking to a http server hosting the torrent contents. This is useful if the torrent tracker is ever unreachable. Example:: -w url1 url2 url3 Edit Usage ===== torrentfile e [-h] [--tracker [ ...]] [--web-seed [ ...]] [--private] [--comment ] [--source ] <*.torrent> Positional Arguments -------------------- <*.torrent> path to *.torrent file Optional Arguments ------------------ -h, --help show this help message and exit --tracker [ ...] Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). --web-seed [ ...] Replace current list of web-seed urls with one or more space seperated url(s) --private Make torrent private. --comment Replaces any existing comment with --source Replaces current source with Recheck Usage ===== torrentfile r [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> path to .torrent file. path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit Magnet Usage ===== torrentfile m [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> Path to Bittorrent meta file. Optional Arguments ------------------ -h, --help show this help message and exit","title":"Help"},{"location":"Commands/#torrentfile-cli-menu","text":"","title":"TorrentFile CLI Menu"},{"location":"Commands/#help-messages","text":"","title":"Help Messages"},{"location":"Commands/#main","text":"Usage ===== torrentfile [-h] [-i] [-V] [-v] ... CLI Tool for creating, checking, editing... Bittorrent meta files. TorrentFile supports all versions of torrent files. Options ------- -h, --help show this help message and exit -i, --interactive select program options interactively -V, --version show program version and exit -v, --verbose output debug information Actions ------- c (create, new) Create a torrent meta file. e (edit) Edit existing torrent meta file. m (magnet) Create magnet url from an existing Bittorrent meta file. r (recheck, check) Calculate amount of torrent meta file's content is found on disk. i (info) Show detailed information about a torrent file.","title":"Main"},{"location":"Commands/#create","text":"Usage ===== torrentfile c [-h] [-a [ ...]] [-p] [-s ] [-m] [-c ] [-o ] [-t [ ...]] [--noprogress] [--meta-version ] [--piece-length ] [-w [ ...]] Positional Arguments -------------------- Path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit -a [ ...], --announce [ ...] Alias for -t/--tracker -p, --private Create a private torrent file -s , --source Useful for cross-seeding -m, --magnet Output Magnet Link after creation completes -c , --comment Include a comment in file metadata -o , --out Output path for created .torrent file -t [ ...], --tracker [ ...] One or more Bittorrent tracker announce url(s). --prog, --progress (0) = no progress bar displayed (1) = progress bar is displayed (default) --meta-version Bittorrent metafile version. Options - {1, 2, 3} (1) = Bittorrent v1 (Default) (2) = Bittorrent v2 (3) = Bittorrent v1 & v2 hybrid --piece-length Number of bytes per piece. (Default: None) Acceptable inputs include {14 - 24} as exponent for 2^n, or any acceptable integer value (must be power of 2). Examples:: [--piece-length 14] [--piece-length 16777216] -w [ ...], --web-seed [ ...] One or more url(s) linking to a http server hosting the torrent contents. This is useful if the torrent tracker is ever unreachable. Example:: -w url1 url2 url3","title":"Create"},{"location":"Commands/#edit","text":"Usage ===== torrentfile e [-h] [--tracker [ ...]] [--web-seed [ ...]] [--private] [--comment ] [--source ] <*.torrent> Positional Arguments -------------------- <*.torrent> path to *.torrent file Optional Arguments ------------------ -h, --help show this help message and exit --tracker [ ...] Replace current list of tracker/announce urls with one or more space seperated Bittorrent tracker announce url(s). --web-seed [ ...] Replace current list of web-seed urls with one or more space seperated url(s) --private Make torrent private. --comment Replaces any existing comment with --source Replaces current source with ","title":"Edit"},{"location":"Commands/#recheck","text":"Usage ===== torrentfile r [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> path to .torrent file. path to content file or directory Optional Arguments ------------------ -h, --help show this help message and exit","title":"Recheck"},{"location":"Commands/#magnet","text":"Usage ===== torrentfile m [-h] <*.torrent> Positional Arguments -------------------- <*.torrent> Path to Bittorrent meta file. Optional Arguments ------------------ -h, --help show this help message and exit","title":"Magnet"},{"location":"changelog/","text":"TorrentFile Version 0.7.9 complete rewrite of the recheck procedures Recheck now provides more accuracy and more details improvements to the new custom progressbar changed the cli argument for the progress bar the options are now just 0 and 1 included new unit tests for all new features marked unused functions as deprecated added a new hasher object for v2 and hybrid torrents minor bug fixes and styling changes Version 0.7.8 more updates to logging major improvements to progress bar removed tqdm as dependency implemented custom progress bar logic new cli argument controlling the progress bar support for pyben 0.3.1 added threading to recheck module added mixins module unit test updates and improvements Version 0.7.5 updates to logging facility fixed bug in created hybrid torrent files fixed cli when subcomman not chosen doc updates unit test updates and improvements Version 0.7.2 cleaned up readme and help messages removed useless print statements improved CI tooling and checking minor bug fixes Version 0.7.1 split CI integration into separate platform specific files added new cli argument --cwd which changes the default save to location to the current working directory (this will be default in future) added unit tests to cover the new argument Changed license to a the more permissive Apache 2 software license Version 0.7.0 Fixed issues with logging to file. Finished adding tests for Unicode Support Deprecated some unneccessary code Clean up documentation and README removed config files no longer in use. Version 0.6.13 Fixed bug that created a torrent file with no name. Fixed bug that would error if cli path was listed after announce urls Added full unicode support. Added Unittests for new features and bug fixes Version 0.6.11 Fixed bug that occured during recheck when file of 0 length is included. Altered Recheck algorithm to process 0 length files. Only effected meta version 2 and hybrid torrent files. Added unittests to cover the situation. Version 0.6.10 Updates to documentation Integrated Type hints in source code Updated build and CI process Version 0.6.9 The --progress flag is now --noprogress Default behavior is to show progress bar use --noprogress to not show added CLI Help format strings added custom CLI help formatter class Titled Help Message section headers Fixed a bunch of error pages created by mkdocs Version 0.6.8 Documentation for newest features CLI usage examples Improved unittests made progress bar active by default Version 0.6.7 Updates to API Version 0.6.6 bug that created faulty Bittorrent V2 meta files in some instances. back to working as it should. Version 0.6.5 Support for creating Magnet URI's Added optional progress bar for torrent creation Log File handler CLI args page in documentation verbose and logging bugs multi tracker errors bug Version 0.6.4 CLI interface add subcommands added interactive mode Re-wrote the recheck module fixed documentation and docstrings linting and testing errors Version 0.6.3 Fixed Bug that would format list of trackers incorrectly CLI Bug Fixes Version 0.6.2 Bug fixes Documentation error pages Version 0.6.0 cli commands alterations debug logging during creation process Version 0.5.2 Fixed Bug that was adding wrong fields to info dict Version 0.5.0 Slew of new unit tests Stricter linting features Alternative method of -re-check feature Bug Fixes CLI help formatting errors Version 0.4.8 Improved Algorithm performance for ReCheck. Additions to documentation. Version 0.4.7 Fixed A bug that misspelled a field when creating Hybrid torrent files. Re-Check procedure for v2 and hybrid torrent file checking. Version 0.4.6 CLI Help and Usage Messages. Expanded CLI args. Completely new CheckerClass which replaces old Checker Hooks for GUI or other 3rd party apps to hook into Checking Documentation and Unit tests for new CheckerClass Version 0.4.5 Documentation and docstrings improvements Better code formating and more detailed docstrings More unit tests. Version 0.4.2 The ReChecker feature now supports v1, v2, & hybrid .torrent file. Bug in CLI for python < 3.8 Version 0.4.1 Added tests for hybrid class Added logging features new cli flag to activate debug mode Documentation theme. Fixed Bug that allowed improper piece length values. Version 0.4.0 Fixed bugs in creating hybrid files. Bug Fix that broke cli. Version 0.3.0 Added/Improved support for hybrid meta files. Many additions to testing suit including linting and coverage tests. Version 0.2.8 Styling fixes. Bug Fixes. Prelimenary support for bittorrent hybrid meta files. Bug Fixes Version 0.2.7 major imporvements to torrentfile-GUI. minor adjustments to this package for integration. Code consolidation Bug Fixes Documentation additions Implemented CI/CD Integration Version 0.2.3 Bug Fixes Code Style and Formatting Added more unittests Version 0.2.1 Bittorrent Protocol V2 Support v2 metafile options to cli v2 metafile tests Version 0.1.7 Docstrings Improvements. Added documentation rederer. Improved readme file. formatting Version 0.1.2 Added a Command Line Interface Rough Graphical User Interface Minor Bug Fixes Improved unittest coverage Version 0.1.0 added SHA256 support Feeder class for seemless file switching Fixed the primary entrypoint function. Improved docstrings Bug fixes Version 0.0.2 Added Unittests added bencode support added hashing support Version 0.0.1 Initial concept and planning","title":"Changelog"},{"location":"changelog/#torrentfile","text":"","title":"TorrentFile"},{"location":"changelog/#version-079","text":"complete rewrite of the recheck procedures Recheck now provides more accuracy and more details improvements to the new custom progressbar changed the cli argument for the progress bar the options are now just 0 and 1 included new unit tests for all new features marked unused functions as deprecated added a new hasher object for v2 and hybrid torrents minor bug fixes and styling changes","title":"Version 0.7.9"},{"location":"changelog/#version-078","text":"more updates to logging major improvements to progress bar removed tqdm as dependency implemented custom progress bar logic new cli argument controlling the progress bar support for pyben 0.3.1 added threading to recheck module added mixins module unit test updates and improvements","title":"Version 0.7.8"},{"location":"changelog/#version-075","text":"updates to logging facility fixed bug in created hybrid torrent files fixed cli when subcomman not chosen doc updates unit test updates and improvements","title":"Version 0.7.5"},{"location":"changelog/#version-072","text":"cleaned up readme and help messages removed useless print statements improved CI tooling and checking minor bug fixes","title":"Version 0.7.2"},{"location":"changelog/#version-071","text":"split CI integration into separate platform specific files added new cli argument --cwd which changes the default save to location to the current working directory (this will be default in future) added unit tests to cover the new argument Changed license to a the more permissive Apache 2 software license","title":"Version 0.7.1"},{"location":"changelog/#version-070","text":"Fixed issues with logging to file. Finished adding tests for Unicode Support Deprecated some unneccessary code Clean up documentation and README removed config files no longer in use.","title":"Version 0.7.0"},{"location":"changelog/#version-0613","text":"Fixed bug that created a torrent file with no name. Fixed bug that would error if cli path was listed after announce urls Added full unicode support. Added Unittests for new features and bug fixes","title":"Version 0.6.13"},{"location":"changelog/#version-0611","text":"Fixed bug that occured during recheck when file of 0 length is included. Altered Recheck algorithm to process 0 length files. Only effected meta version 2 and hybrid torrent files. Added unittests to cover the situation.","title":"Version 0.6.11"},{"location":"changelog/#version-0610","text":"Updates to documentation Integrated Type hints in source code Updated build and CI process","title":"Version 0.6.10"},{"location":"changelog/#version-069","text":"The --progress flag is now --noprogress Default behavior is to show progress bar use --noprogress to not show added CLI Help format strings added custom CLI help formatter class Titled Help Message section headers Fixed a bunch of error pages created by mkdocs","title":"Version 0.6.9"},{"location":"changelog/#version-068","text":"Documentation for newest features CLI usage examples Improved unittests made progress bar active by default","title":"Version 0.6.8"},{"location":"changelog/#version-067","text":"Updates to API","title":"Version 0.6.7"},{"location":"changelog/#version-066","text":"bug that created faulty Bittorrent V2 meta files in some instances. back to working as it should.","title":"Version 0.6.6"},{"location":"changelog/#version-065","text":"Support for creating Magnet URI's Added optional progress bar for torrent creation Log File handler CLI args page in documentation verbose and logging bugs multi tracker errors bug","title":"Version 0.6.5"},{"location":"changelog/#version-064","text":"CLI interface add subcommands added interactive mode Re-wrote the recheck module fixed documentation and docstrings linting and testing errors","title":"Version 0.6.4"},{"location":"changelog/#version-063","text":"Fixed Bug that would format list of trackers incorrectly CLI Bug Fixes","title":"Version 0.6.3"},{"location":"changelog/#version-062","text":"Bug fixes Documentation error pages","title":"Version 0.6.2"},{"location":"changelog/#version-060","text":"cli commands alterations debug logging during creation process","title":"Version 0.6.0"},{"location":"changelog/#version-052","text":"Fixed Bug that was adding wrong fields to info dict","title":"Version 0.5.2"},{"location":"changelog/#version-050","text":"Slew of new unit tests Stricter linting features Alternative method of -re-check feature Bug Fixes CLI help formatting errors","title":"Version 0.5.0"},{"location":"changelog/#version-048","text":"Improved Algorithm performance for ReCheck. Additions to documentation.","title":"Version 0.4.8"},{"location":"changelog/#version-047","text":"Fixed A bug that misspelled a field when creating Hybrid torrent files. Re-Check procedure for v2 and hybrid torrent file checking.","title":"Version 0.4.7"},{"location":"changelog/#version-046","text":"CLI Help and Usage Messages. Expanded CLI args. Completely new CheckerClass which replaces old Checker Hooks for GUI or other 3rd party apps to hook into Checking Documentation and Unit tests for new CheckerClass","title":"Version 0.4.6"},{"location":"changelog/#version-045","text":"Documentation and docstrings improvements Better code formating and more detailed docstrings More unit tests.","title":"Version 0.4.5"},{"location":"changelog/#version-042","text":"The ReChecker feature now supports v1, v2, & hybrid .torrent file. Bug in CLI for python < 3.8","title":"Version 0.4.2"},{"location":"changelog/#version-041","text":"Added tests for hybrid class Added logging features new cli flag to activate debug mode Documentation theme. Fixed Bug that allowed improper piece length values.","title":"Version 0.4.1"},{"location":"changelog/#version-040","text":"Fixed bugs in creating hybrid files. Bug Fix that broke cli.","title":"Version 0.4.0"},{"location":"changelog/#version-030","text":"Added/Improved support for hybrid meta files. Many additions to testing suit including linting and coverage tests.","title":"Version 0.3.0"},{"location":"changelog/#version-028","text":"Styling fixes. Bug Fixes. Prelimenary support for bittorrent hybrid meta files. Bug Fixes","title":"Version 0.2.8"},{"location":"changelog/#version-027","text":"major imporvements to torrentfile-GUI. minor adjustments to this package for integration. Code consolidation Bug Fixes Documentation additions Implemented CI/CD Integration","title":"Version 0.2.7"},{"location":"changelog/#version-023","text":"Bug Fixes Code Style and Formatting Added more unittests","title":"Version 0.2.3"},{"location":"changelog/#version-021","text":"Bittorrent Protocol V2 Support v2 metafile options to cli v2 metafile tests","title":"Version 0.2.1"},{"location":"changelog/#version-017","text":"Docstrings Improvements. Added documentation rederer. Improved readme file. formatting","title":"Version 0.1.7"},{"location":"changelog/#version-012","text":"Added a Command Line Interface Rough Graphical User Interface Minor Bug Fixes Improved unittest coverage","title":"Version 0.1.2"},{"location":"changelog/#version-010","text":"added SHA256 support Feeder class for seemless file switching Fixed the primary entrypoint function. Improved docstrings Bug fixes","title":"Version 0.1.0"},{"location":"changelog/#version-002","text":"Added Unittests added bencode support added hashing support","title":"Version 0.0.2"},{"location":"changelog/#version-001","text":"Initial concept and planning","title":"Version 0.0.1"},{"location":"man/","text":"Name torrentfile Synopsis torrentfile [options] [options] Description torrentfile is a CLI tool for creating, editing, validating, or reviewing Bittorrent files(.torrent). It supports all current versions of the Bittorrent Protocol files as well as hybrid files. Has support for generating magnet URI's for meta files. Options -h displays all relevant command line options and subcommands. -V displays program and version. -v enables debug mode and outputs a large amount of information to the terminal. -i activates interactive mode for selecting subcommands and options. Sub-commands create alias: c , new torrentfile create [options] torrentfile c [options] The create subcommand is used for generating new torrent files. The only required argument is the path( ) to the contents file or directory. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. --piece-length Uses the number that follows as the piece-length size for the newly created torrent file. If option isn't used the program will pick the ideal size. Acceptable values include 14-29 which is interpreted as the number to raise 2 by (e.g. 14 is interpreted as 16384), or any perfect power of 2 greater than or equal to 16 KiB and less than 1 GiB. Example: --piece-length 14 or --piece-length 16384 . --meta-version Use the following number as the Bittorrent version the torrent will be used on. The default is 1. Options: 1 - Bittorrent v1, 2 - Bittorrent v2, 3 - Bittorrent v1 & v2 Examples: --meta-version 1 , --meta-version 2 , --meta-version 3 -o --out Specify the full path to the newly created torrent file. The default is to save it adjacent to the content. Example: if content is at /home/user/torrents/content the default would create /home/user/torrents/content.torrent --cwd --current Changes the default save location to the current working directory. Example: if content is at /home/user/torrents/content this option would create ./content.torrent --progress --prog Options (0, 1): No status bar will be shown if 0. Otherwise the default and 1 argument means progress bar is shown info alias: i torrentfile info torrentfile i There are no optional arguments for the info subcommand. /path/to/*.torrent The relative or absolute path to the torrent file. edit alias: e torrentfile edit [options] torrentfile e [options] Each option identifies the field to edit inside the torrent file and what the new value should be. If an option is not used then its field will be ommited in the newly created torrent file. As such if the file is marked as private and it should remain that way, the -p option should be used. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. recheck alias: r , check torrentfile recheck torrentfile r There are only two arguments for the recheck command and both are mandatory. The first is the absolute or relative to the torrent file, and the second is the absolute or relative to it's content. This will display a progress bar and at the end output what percent of the torrentfile's content it found at the path indicated. It is also permitted to use the contents parent directory as the second argument and the result will be the same.","title":"Details"},{"location":"man/#name","text":"torrentfile","title":"Name"},{"location":"man/#synopsis","text":"torrentfile [options] [options] ","title":"Synopsis"},{"location":"man/#description","text":"torrentfile is a CLI tool for creating, editing, validating, or reviewing Bittorrent files(.torrent). It supports all current versions of the Bittorrent Protocol files as well as hybrid files. Has support for generating magnet URI's for meta files.","title":"Description"},{"location":"man/#options","text":"-h displays all relevant command line options and subcommands. -V displays program and version. -v enables debug mode and outputs a large amount of information to the terminal. -i activates interactive mode for selecting subcommands and options.","title":"Options"},{"location":"man/#sub-commands","text":"","title":"Sub-commands"},{"location":"man/#create","text":"alias: c , new torrentfile create [options] torrentfile c [options] The create subcommand is used for generating new torrent files. The only required argument is the path( ) to the contents file or directory. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT. --piece-length Uses the number that follows as the piece-length size for the newly created torrent file. If option isn't used the program will pick the ideal size. Acceptable values include 14-29 which is interpreted as the number to raise 2 by (e.g. 14 is interpreted as 16384), or any perfect power of 2 greater than or equal to 16 KiB and less than 1 GiB. Example: --piece-length 14 or --piece-length 16384 . --meta-version Use the following number as the Bittorrent version the torrent will be used on. The default is 1. Options: 1 - Bittorrent v1, 2 - Bittorrent v2, 3 - Bittorrent v1 & v2 Examples: --meta-version 1 , --meta-version 2 , --meta-version 3 -o --out Specify the full path to the newly created torrent file. The default is to save it adjacent to the content. Example: if content is at /home/user/torrents/content the default would create /home/user/torrents/content.torrent --cwd --current Changes the default save location to the current working directory. Example: if content is at /home/user/torrents/content this option would create ./content.torrent --progress --prog Options (0, 1): No status bar will be shown if 0. Otherwise the default and 1 argument means progress bar is shown","title":"create"},{"location":"man/#info","text":"alias: i torrentfile info torrentfile i There are no optional arguments for the info subcommand. /path/to/*.torrent The relative or absolute path to the torrent file.","title":"info"},{"location":"man/#edit","text":"alias: e torrentfile edit [options] torrentfile e [options] Each option identifies the field to edit inside the torrent file and what the new value should be. If an option is not used then its field will be ommited in the newly created torrent file. As such if the file is marked as private and it should remain that way, the -p option should be used. -a -t --announce --tracker Adds the list of url's that follow to the list of trackers for the newly created torrent file. Example -t http://url1 https://url2 ... -w --web-seeds Adds the list of urls that follow to the list of web-seed addresses for the newly created torrent file. --comment Includes the comment that follows to the metadata saved in the newly created torrent file. Example: --comment \"Created for MyTrackerExample.com\" --source Creates a source field in the info dictionary with the string that follows as the value. Frequently used for cross-seeding on private trackers. Example: --source MyTrackerExample -p --private Indicates that the torrent will be used on a private tracker. Disables multi-tracker protocols and DHT.","title":"edit"},{"location":"man/#recheck","text":"alias: r , check torrentfile recheck torrentfile r There are only two arguments for the recheck command and both are mandatory. The first is the absolute or relative to the torrent file, and the second is the absolute or relative to it's content. This will display a progress bar and at the end output what percent of the torrentfile's content it found at the path indicated. It is also permitted to use the contents parent directory as the second argument and the result will be the same.","title":"recheck"},{"location":"source/","text":"TorrentFile API and Source Torrent Module module torrentfile. torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. Classes MetaFile \u2014 Base Class for all TorrentFile classes. TorrentFile \u2014 Class for creating Bittorrent meta files. TorrentFileV2 \u2014 Class for creating Bittorrent meta v2 files. TorrentFileHybrid \u2014 Construct the Hybrid torrent meta file with provided parameters. TorrentAssembler \u2014 Assembler class for Bittorrent version 2 and hybrid meta files. ------ Utils Module module torrentfile. utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Classes Memo \u2014 Memoice chache object. MissingPathError \u2014 Path parameter is required to specify target content. PieceLengthValueError \u2014 Piece Length parameter must equal a perfect power of 2. Functions get_file_list ( path ) (list) \u2014 Return a sorted list of file paths contained in directory. get_piece_length ( size ) (int) \u2014 Calculate the ideal piece length for bittorrent data. humanize_bytes ( amount ) (str) \u2014 Convert integer into human readable memory sized denomination. next_power_2 ( value ) (int) \u2014 Calculate the next perfect power of 2 equal to or greater than value. normalize_piece_length ( piece_length ) (int) \u2014 Verify input piece_length is valid and convert accordingly. path_piece_length ( path ) (int) \u2014 Calculate piece length for input path and contents. path_size ( path ) (int) \u2014 Return the total size of all files in path recursively. path_stat ( path ) (list) \u2014 Calculate directory statistics. ------ Edit Module module torrentfile. edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds Functions edit_torrent ( metafile , args ) (dict) \u2014 Edit the properties and values in a torrent meta file. filter_empty ( args , meta , info ) \u2014 Remove dictionary keys with empty values. ------ Interactive Module module torrentfile. interactive Module contains the procedures used for Interactive Mode. Classes InteractiveEditor \u2014 Interactive dialog class for torrent editing. InteractiveCreator \u2014 Class namespace for interactive program options. Functions create_torrent ( ) \u2014 Create new torrent file interactively. edit_action ( ) \u2014 Edit the editable values of the torrent meta file. get_input ( *args ) (str) \u2014 Determine appropriate input function to call. recheck_torrent ( ) \u2014 Check torrent download completed percentage. select_action ( ) \u2014 Operate TorrentFile program interactively through terminal. showcenter ( txt ) \u2014 Print text to screen in the center position of the terminal. showtext ( txt ) \u2014 Print contents of txt to screen. ------ CLI Module module torrentfile. cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. Classes TorrentFileHelpFormatter \u2014 Formatting class for help tips provided by the CLI. Functions activate_logger ( ) \u2014 Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. main ( ) \u2014 Initiate main function for CLI script. ------ Recheck Module module torrentfile. recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Classes Checker \u2014 Check a given file or directory to see if it matches a torrentfile. FeedChecker \u2014 Validates torrent content. HashChecker \u2014 Iterate through contents of meta data and verify with file contents. ------ Hasher Module module torrentfile. hasher Piece/File Hashers for Bittorrent meta file contents. Classes Hasher \u2014 Piece hasher for Bittorrent V1 files. HasherV2 \u2014 Calculate the root hash and piece layers for file contents. HasherHybrid \u2014 Calculate root and piece hashes for creating hybrid torrent file. FileHasher \u2014 Calculate root and piece hashes for creating hybrid torrent file. Functions merkle_root ( blocks ) (bytes) \u2014 Calculate the merkle root for a seq of sha256 hash digests. ------ Torrentfile can create Bittorrent metafiles for any content. Both Bittorrent v1 and v2 are fully supported. Also included is a torrent torrent file checker, which can verify a .torrent file is formated correctly as well as validate files and folders against metadata. Modules: metafile: Creation/Validation of v1 .torrent files. metafile2: Creation/Validation of v2 .torrent files. torrentfile: torrentfiles Command Line Interface implementation. exceptions: Custom Exceptions used in package. utils: Utilities used throughout package. create ( args ) Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. info ( args ) Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required magnet ( metafile ) Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. __main__ Enable calling the package directly with python from the command line. cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. TorrentFileHelpFormatter Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. __init__ ( prog , width = 40 , max_help_positions = 30 ) Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 _format_text ( text ) Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. _join_parts ( part_strings ) Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. _split_lines ( text , _ ) Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required format_headers ( parts ) staticmethod Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. activate_logger () Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. main () Initiate main function for CLI script. commands The commands module contains the Action Commands executed by the CLI script. Each function pertains to a command line action/subcommand and drives specific features of the application. Functions create_command info_command edit_command recheck_command magnet_command create ( args ) Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents. edit ( args ) Execute the edit CLI sub-command with provided arguments. Parameters: Name Type Description Default args Namespace positional and optional CLI arguments. required Returns: Type Description str path to edited torrent file. info ( args ) Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required magnet ( metafile ) Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI. recheck ( args ) Execute recheck CLI sub-command. Parameters: Name Type Description Default args Namespace positional and optional arguments. required Returns: Type Description str The percentage of content currently saved to disk. edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds edit_torrent ( metafile , args ) Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. filter_empty ( args , meta , info ) Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required hasher Piece/File Hashers for Bittorrent meta file contents. FileHasher Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True __init__ ( path , piece_length , progress = True , hybrid = False ) Construct Hasher class instances for each file in torrent. __iter__ () Return self : needed to implement iterator implementation. __next__ () Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. _calculate_root () Calculate the root hash for opened file. _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Hasher Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True __init__ ( paths , piece_length , progress = True ) Generate hashes of piece length data from filelist contents. __iter__ () Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. __next__ () Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. _handle_partial ( arr ) Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. next_file () Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. HasherHybrid Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True __init__ ( path , piece_length , progress = True ) Construct Hasher class instances for each file in torrent. DEPRECATED _calculate_root () Calculate the root hash for opened file. DEPRECATED _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. process_file ( data ) Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required HasherV2 Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True __init__ ( path , piece_length , progress = True ) Calculate and store hash information for specific file. DEPRECATED _calculate_root () Calculate root hash for the target file. DEPRECATED process_file ( fd ) Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required merkle_root ( blocks ) Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. interactive Module contains the procedures used for Interactive Mode. InteractiveCreator Class namespace for interactive program options. __init__ () Initialize interactive meta file creator dialog. get_props () Gather details for torrentfile from user. InteractiveEditor Interactive dialog class for torrent editing. __init__ ( metafile ) Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required edit_props () Loop continuosly for edits until user signals DONE. sanatize_response ( key , response ) Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required show_current () Display the current met file information to screen. _get_input ( txt ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. _get_input_loop ( txt , func ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. create_torrent () Create new torrent file interactively. edit_action () Edit the editable values of the torrent meta file. get_input ( * args ) Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. recheck_torrent () Check torrent download completed percentage. select_action () Operate TorrentFile program interactively through terminal. showcenter ( txt ) Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required showtext ( txt ) Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required mixins Collection of classes that can be used as Mixins with other base classes. Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the progress bar mixin. And any class is eligible to use the callback mixin. CbMixin Mixin class to set a callback during hashing procedure. set_callback ( func ) classmethod Assign a callback to the Hashing class. Parameters: Name Type Description Default func function the callback function required ProgMixin Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files. Methods prog_start prog_update prog_close is_active () Test to see if there is an active progress bar for object. Returns: Name Type Description bool True if there is, otherwise False. prog_close () Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one. prog_start ( total , path , length = 50 , unit = None ) Generate a new progress bar for the given file path. Parameters: Name Type Description Default total int the total amount of units accumulating towards. required path str path to file being hashed. required length int the number of characters of the actual progress bar. 50 unit str the text representation of the value being measured. None prog_update ( val ) Update progress bar with given amount of progress. Parameters: Name Type Description Default val int the number of bytes count the progress bar should increase. required ProgressBar Holds the state and details of the terminal progress bars. Parameters: Name Type Description Default total int the total amount to be accumulated. required title str the subject of the progress tracker required length int the width of the progress bar required unit str the text representation incremented required __init__ ( total , title , length , unit , start ) Construct the progress bar object and store state of it's properties. increment ( value ) Increase the state of the progress bar value. Parameters: Name Type Description Default value int the amount to increment the state by. required pbar () Return the size of the filled portion of the progress bar. Returns: Name Type Description str the progress bar characters waiting ( msg , flag , timeout = 180 ) Show loading message while thread completes processing. Parameters: Name Type Description Default msg str Message string printed before the progress bar required flag list Once flag is filled exit loop required timeout int max amount of time to run the function. 180 recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Checker Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required Example >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) __init__ ( metafile , path ) Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required check_paths () Gather all file paths described in the torrent file. find_root ( path ) Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content iter_hashes () Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece log_msg ( * args , level = logging . INFO ) Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO piece_checker () Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. register_callback ( hook ) classmethod Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required results () Generate result percentage and store for future calls. walk_file_tree ( tree , partials ) Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required FeedChecker Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required __init__ ( checker ) Generate hashes of piece length data from filelist contents. __iter__ () Assign iterator and return self. __next__ () Yield back result of comparison. _gen_padding ( partial , length , read = 0 ) Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. extract ( path , partial ) Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. iter_pieces () Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. HashChecker Bases: ProgMixin Iterate through contents of meta data and verify with file contents. Parameters: Name Type Description Default checker Checker the checker instance that maintains variables. required Padder Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required __init__ ( length , piece_length ) Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required __iter__ () Return self to correctly implement iterator type. __next__ () Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration __init__ ( checker ) Construct a HybridChecker instance. __iter__ () Assign iterator and return self. __next__ () Provide the result of comparison. advance () Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size next_file () Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found process_current () Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration torrent Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. MetaFile Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None __init__ ( path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ ) Construct MetaFile superclass and assign local attributes. assemble () Overload in subclasses. Raises: Type Description Exception NotImplementedError set_callback ( func ) classmethod Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required sort_meta () Sort the info and meta dictionaries. write ( outfile = None ) Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. TorrentAssembler Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. _traverse ( path ) Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required assemble () Assemble the parts of the torrentfile into meta dictionary. TorrentFile Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} __init__ ( ** kwargs ) Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} assemble () Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file TorrentFileHybrid Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. _traverse ( path ) Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required assemble () Assemble the parts of the torrentfile into meta dictionary. DEPRECATED TorrentFileV2 Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} __init__ ( ** kwargs ) Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} _traverse ( path ) Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required assemble () Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Memo Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required __call__ ( path ) Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. __init__ ( func ) Construct for memoization. MissingPathError Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None __init__ ( message = None ) Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. PieceLengthValueError Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None __init__ ( message = None ) Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. _filelist_total ( path ) Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. filelist_total ( pathstring ) Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. get_file_list ( path ) Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. get_piece_length ( size ) Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. humanize_bytes ( amount ) Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. next_power_2 ( value ) Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. normalize_piece_length ( piece_length ) Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. path_piece_length ( path ) Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. path_size ( path ) Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. path_stat ( path ) Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. version Holds the release version number. Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. TorrentFileHelpFormatter Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter. __init__ ( prog , width = 40 , max_help_positions = 30 ) Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30 _format_text ( text ) Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input. _join_parts ( part_strings ) Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI. _split_lines ( text , _ ) Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required format_headers ( parts ) staticmethod Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers. activate_logger () Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args = None ) Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were. main () Initiate main function for CLI script. Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent. Keywords private comment source trackers web-seeds edit_torrent ( metafile , args ) Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries. filter_empty ( args , meta , info ) Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Piece/File Hashers for Bittorrent meta file contents. FileHasher Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True __init__ ( path , piece_length , progress = True , hybrid = False ) Construct Hasher class instances for each file in torrent. __iter__ () Return self : needed to implement iterator implementation. __next__ () Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash. _calculate_root () Calculate the root hash for opened file. _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. Hasher Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True __init__ ( paths , piece_length , progress = True ) Generate hashes of piece length data from filelist contents. __iter__ () Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces. __next__ () Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted. _handle_partial ( arr ) Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece. next_file () Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False. HasherHybrid Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True __init__ ( path , piece_length , progress = True ) Construct Hasher class instances for each file in torrent. DEPRECATED _calculate_root () Calculate the root hash for opened file. DEPRECATED _pad_remaining ( block_count ) Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree. process_file ( data ) Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required HasherV2 Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True __init__ ( path , piece_length , progress = True ) Calculate and store hash information for specific file. DEPRECATED _calculate_root () Calculate root hash for the target file. DEPRECATED process_file ( fd ) Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required merkle_root ( blocks ) Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Module contains the procedures used for Interactive Mode. InteractiveCreator Class namespace for interactive program options. __init__ () Initialize interactive meta file creator dialog. get_props () Gather details for torrentfile from user. InteractiveEditor Interactive dialog class for torrent editing. __init__ ( metafile ) Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required edit_props () Loop continuosly for edits until user signals DONE. sanatize_response ( key , response ) Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required show_current () Display the current met file information to screen. _get_input ( txt ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user. _get_input_loop ( txt , func ) Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user. create_torrent () Create new torrent file interactively. edit_action () Edit the editable values of the torrent meta file. get_input ( * args ) Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call. recheck_torrent () Check torrent download completed percentage. select_action () Operate TorrentFile program interactively through terminal. showcenter ( txt ) Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required showtext ( txt ) Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Checker Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required Example >> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location) __init__ ( metafile , path ) Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required check_paths () Gather all file paths described in the torrent file. find_root ( path ) Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content iter_hashes () Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece log_msg ( * args , level = logging . INFO ) Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO piece_checker () Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher. register_callback ( hook ) classmethod Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required results () Generate result percentage and store for future calls. walk_file_tree ( tree , partials ) Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required FeedChecker Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required __init__ ( checker ) Generate hashes of piece length data from filelist contents. __iter__ () Assign iterator and return self. __next__ () Yield back result of comparison. _gen_padding ( partial , length , read = 0 ) Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros. extract ( path , partial ) Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents. iter_pieces () Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data. HashChecker Bases: ProgMixin Iterate through contents of meta data and verify with file contents. Parameters: Name Type Description Default checker Checker the checker instance that maintains variables. required Padder Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required __init__ ( length , piece_length ) Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required __iter__ () Return self to correctly implement iterator type. __next__ () Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration __init__ ( checker ) Construct a HybridChecker instance. __iter__ () Assign iterator and return self. __next__ () Provide the result of comparison. advance () Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size next_file () Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found process_current () Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Classes and procedures pertaining to the creation of torrent meta files. Classes TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes. Constants BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash. Bittorrent V2 From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded. Meta Version 2 Dictionary: \"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key. Bittorrent V1 v1 meta-dictionary announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. MetaFile Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None __init__ ( path = None , announce = None , comment = None , piece_length = None , private = False , outfile = None , source = None , progress = 1 , cwd = False , httpseeds = None , url_list = None , content = None , meta_version = None , ** _ ) Construct MetaFile superclass and assign local attributes. assemble () Overload in subclasses. Raises: Type Description Exception NotImplementedError set_callback ( func ) classmethod Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required sort_meta () Sort the info and meta dictionaries. write ( outfile = None ) Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information. TorrentAssembler Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. _traverse ( path ) Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required assemble () Assemble the parts of the torrentfile into meta dictionary. TorrentFile Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {} __init__ ( ** kwargs ) Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {} assemble () Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file TorrentFileHybrid Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {} __init__ ( ** kwargs ) Create Bittorrent v1 v2 hybrid metafiles. _traverse ( path ) Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required assemble () Assemble the parts of the torrentfile into meta dictionary. DEPRECATED TorrentFileV2 Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {} __init__ ( ** kwargs ) Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {} _traverse ( path ) Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required assemble () Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Memo Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required __call__ ( path ) Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path. __init__ ( func ) Construct for memoization. MissingPathError Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None __init__ ( message = None ) Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class. PieceLengthValueError Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None __init__ ( message = None ) Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class. _filelist_total ( path ) Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree. filelist_total ( pathstring ) Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found. get_file_list ( path ) Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths. get_piece_length ( size ) Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length. humanize_bytes ( amount ) Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes. next_power_2 ( value ) Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2. normalize_piece_length ( piece_length ) Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value. path_piece_length ( path ) Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content. path_size ( path ) Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files. path_stat ( path ) Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Holds the release version number.","title":"API"},{"location":"source/#torrentfile_1","text":"","title":"TorrentFile"},{"location":"source/#api-and-source","text":"","title":"API and Source"},{"location":"source/#torrent-module","text":"module torrentfile. torrent Classes and procedures pertaining to the creation of torrent meta files.","title":"Torrent Module"},{"location":"source/#classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory. Classes MetaFile \u2014 Base Class for all TorrentFile classes. TorrentFile \u2014 Class for creating Bittorrent meta files. TorrentFileV2 \u2014 Class for creating Bittorrent meta v2 files. TorrentFileHybrid \u2014 Construct the Hybrid torrent meta file with provided parameters. TorrentAssembler \u2014 Assembler class for Bittorrent version 2 and hybrid meta files.","title":"v1 meta-dictionary"},{"location":"source/#-","text":"","title":"------"},{"location":"source/#utils-module","text":"module torrentfile. utils Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field. Classes Memo \u2014 Memoice chache object. MissingPathError \u2014 Path parameter is required to specify target content. PieceLengthValueError \u2014 Piece Length parameter must equal a perfect power of 2. Functions get_file_list ( path ) (list) \u2014 Return a sorted list of file paths contained in directory. get_piece_length ( size ) (int) \u2014 Calculate the ideal piece length for bittorrent data. humanize_bytes ( amount ) (str) \u2014 Convert integer into human readable memory sized denomination. next_power_2 ( value ) (int) \u2014 Calculate the next perfect power of 2 equal to or greater than value. normalize_piece_length ( piece_length ) (int) \u2014 Verify input piece_length is valid and convert accordingly. path_piece_length ( path ) (int) \u2014 Calculate piece length for input path and contents. path_size ( path ) (int) \u2014 Return the total size of all files in path recursively. path_stat ( path ) (list) \u2014 Calculate directory statistics.","title":"Utils Module"},{"location":"source/#-_1","text":"","title":"------"},{"location":"source/#edit-module","text":"module torrentfile. edit Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"Edit Module"},{"location":"source/#keywords","text":"private comment source trackers web-seeds Functions edit_torrent ( metafile , args ) (dict) \u2014 Edit the properties and values in a torrent meta file. filter_empty ( args , meta , info ) \u2014 Remove dictionary keys with empty values.","title":"Keywords"},{"location":"source/#-_2","text":"","title":"------"},{"location":"source/#interactive-module","text":"module torrentfile. interactive Module contains the procedures used for Interactive Mode. Classes InteractiveEditor \u2014 Interactive dialog class for torrent editing. InteractiveCreator \u2014 Class namespace for interactive program options. Functions create_torrent ( ) \u2014 Create new torrent file interactively. edit_action ( ) \u2014 Edit the editable values of the torrent meta file. get_input ( *args ) (str) \u2014 Determine appropriate input function to call. recheck_torrent ( ) \u2014 Check torrent download completed percentage. select_action ( ) \u2014 Operate TorrentFile program interactively through terminal. showcenter ( txt ) \u2014 Print text to screen in the center position of the terminal. showtext ( txt ) \u2014 Print contents of txt to screen.","title":"Interactive Module"},{"location":"source/#-_3","text":"","title":"------"},{"location":"source/#cli-module","text":"module torrentfile. cli Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility. Classes TorrentFileHelpFormatter \u2014 Formatting class for help tips provided by the CLI. Functions activate_logger ( ) \u2014 Activate the builtin logging mechanism when passed debug flag from CLI. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. execute ( args ) (list) \u2014 Initialize Command Line Interface for torrentfile. main ( ) \u2014 Initiate main function for CLI script.","title":"CLI Module"},{"location":"source/#-_4","text":"","title":"------"},{"location":"source/#recheck-module","text":"module torrentfile. recheck Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole. Classes Checker \u2014 Check a given file or directory to see if it matches a torrentfile. FeedChecker \u2014 Validates torrent content. HashChecker \u2014 Iterate through contents of meta data and verify with file contents.","title":"Recheck Module"},{"location":"source/#-_5","text":"","title":"------"},{"location":"source/#hasher-module","text":"module torrentfile. hasher Piece/File Hashers for Bittorrent meta file contents. Classes Hasher \u2014 Piece hasher for Bittorrent V1 files. HasherV2 \u2014 Calculate the root hash and piece layers for file contents. HasherHybrid \u2014 Calculate root and piece hashes for creating hybrid torrent file. FileHasher \u2014 Calculate root and piece hashes for creating hybrid torrent file. Functions merkle_root ( blocks ) (bytes) \u2014 Calculate the merkle root for a seq of sha256 hash digests.","title":"Hasher Module"},{"location":"source/#-_6","text":"Torrentfile can create Bittorrent metafiles for any content. Both Bittorrent v1 and v2 are fully supported. Also included is a torrent torrent file checker, which can verify a .torrent file is formated correctly as well as validate files and folders against metadata. Modules: metafile: Creation/Validation of v1 .torrent files. metafile2: Creation/Validation of v2 .torrent files. torrentfile: torrentfiles Command Line Interface implementation. exceptions: Custom Exceptions used in package. utils: Utilities used throughout package.","title":"------"},{"location":"source/#torrentfile.create","text":"Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents.","title":"create()"},{"location":"source/#torrentfile.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were.","title":"execute()"},{"location":"source/#torrentfile.info","text":"Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required","title":"info()"},{"location":"source/#torrentfile.magnet","text":"Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI.","title":"magnet()"},{"location":"source/#torrentfile.__main__","text":"Enable calling the package directly with python from the command line.","title":"__main__"},{"location":"source/#torrentfile.cli","text":"Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility.","title":"cli"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter","text":"Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter.","title":"TorrentFileHelpFormatter"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.__init__","text":"Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30","title":"__init__()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._format_text","text":"Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input.","title":"_format_text()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._join_parts","text":"Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI.","title":"_join_parts()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._split_lines","text":"Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required","title":"_split_lines()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.format_headers","text":"Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers.","title":"format_headers()"},{"location":"source/#torrentfile.cli.activate_logger","text":"Activate the builtin logging mechanism when passed debug flag from CLI.","title":"activate_logger()"},{"location":"source/#torrentfile.cli.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were.","title":"execute()"},{"location":"source/#torrentfile.cli.main","text":"Initiate main function for CLI script.","title":"main()"},{"location":"source/#torrentfile.commands","text":"The commands module contains the Action Commands executed by the CLI script. Each function pertains to a command line action/subcommand and drives specific features of the application.","title":"commands"},{"location":"source/#torrentfile.commands--functions","text":"create_command info_command edit_command recheck_command magnet_command","title":"Functions"},{"location":"source/#torrentfile.commands.create","text":"Execute the create CLI sub-command to create a new torrent metafile. Parameters: Name Type Description Default args argparse . Namespace positional and optional CLI arguments. required Returns: Type Description torrentfile . MetaFile object containing the path to created metafile and its contents.","title":"create()"},{"location":"source/#torrentfile.commands.edit","text":"Execute the edit CLI sub-command with provided arguments. Parameters: Name Type Description Default args Namespace positional and optional CLI arguments. required Returns: Type Description str path to edited torrent file.","title":"edit()"},{"location":"source/#torrentfile.commands.info","text":"Show torrent metafile details to user via stdout. Parameters: Name Type Description Default args dict command line arguements provided by the user. required","title":"info()"},{"location":"source/#torrentfile.commands.magnet","text":"Create a magnet URI from a Bittorrent meta file. Parameters: Name Type Description Default metafile (Namespace||str) Namespace class for CLI arguments. required Returns: Type Description str created magnet URI.","title":"magnet()"},{"location":"source/#torrentfile.commands.recheck","text":"Execute recheck CLI sub-command. Parameters: Name Type Description Default args Namespace positional and optional arguments. required Returns: Type Description str The percentage of content currently saved to disk.","title":"recheck()"},{"location":"source/#torrentfile.edit","text":"Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"edit"},{"location":"source/#torrentfile.edit--keywords","text":"private comment source trackers web-seeds","title":"Keywords"},{"location":"source/#torrentfile.edit.edit_torrent","text":"Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries.","title":"edit_torrent()"},{"location":"source/#torrentfile.edit.filter_empty","text":"Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required","title":"filter_empty()"},{"location":"source/#torrentfile.hasher","text":"Piece/File Hashers for Bittorrent meta file contents.","title":"hasher"},{"location":"source/#torrentfile.hasher.FileHasher","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True","title":"FileHasher"},{"location":"source/#torrentfile.hasher.FileHasher.__init__","text":"Construct Hasher class instances for each file in torrent.","title":"__init__()"},{"location":"source/#torrentfile.hasher.FileHasher.__iter__","text":"Return self : needed to implement iterator implementation.","title":"__iter__()"},{"location":"source/#torrentfile.hasher.FileHasher.__next__","text":"Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash.","title":"__next__()"},{"location":"source/#torrentfile.hasher.FileHasher._calculate_root","text":"Calculate the root hash for opened file.","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.FileHasher._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree.","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.Hasher","text":"Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True","title":"Hasher"},{"location":"source/#torrentfile.hasher.Hasher.__init__","text":"Generate hashes of piece length data from filelist contents.","title":"__init__()"},{"location":"source/#torrentfile.hasher.Hasher.__iter__","text":"Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces.","title":"__iter__()"},{"location":"source/#torrentfile.hasher.Hasher.__next__","text":"Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted.","title":"__next__()"},{"location":"source/#torrentfile.hasher.Hasher._handle_partial","text":"Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece.","title":"_handle_partial()"},{"location":"source/#torrentfile.hasher.Hasher.next_file","text":"Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False.","title":"next_file()"},{"location":"source/#torrentfile.hasher.HasherHybrid","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True","title":"HasherHybrid"},{"location":"source/#torrentfile.hasher.HasherHybrid.__init__","text":"Construct Hasher class instances for each file in torrent. DEPRECATED","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherHybrid._calculate_root","text":"Calculate the root hash for opened file. DEPRECATED","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherHybrid._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree.","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.HasherHybrid.process_file","text":"Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required","title":"process_file()"},{"location":"source/#torrentfile.hasher.HasherV2","text":"Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True","title":"HasherV2"},{"location":"source/#torrentfile.hasher.HasherV2.__init__","text":"Calculate and store hash information for specific file. DEPRECATED","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherV2._calculate_root","text":"Calculate root hash for the target file. DEPRECATED","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherV2.process_file","text":"Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required","title":"process_file()"},{"location":"source/#torrentfile.hasher.merkle_root","text":"Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree.","title":"merkle_root()"},{"location":"source/#torrentfile.interactive","text":"Module contains the procedures used for Interactive Mode.","title":"interactive"},{"location":"source/#torrentfile.interactive.InteractiveCreator","text":"Class namespace for interactive program options.","title":"InteractiveCreator"},{"location":"source/#torrentfile.interactive.InteractiveCreator.__init__","text":"Initialize interactive meta file creator dialog.","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveCreator.get_props","text":"Gather details for torrentfile from user.","title":"get_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor","text":"Interactive dialog class for torrent editing.","title":"InteractiveEditor"},{"location":"source/#torrentfile.interactive.InteractiveEditor.__init__","text":"Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.edit_props","text":"Loop continuosly for edits until user signals DONE.","title":"edit_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.sanatize_response","text":"Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required","title":"sanatize_response()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.show_current","text":"Display the current met file information to screen.","title":"show_current()"},{"location":"source/#torrentfile.interactive._get_input","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user.","title":"_get_input()"},{"location":"source/#torrentfile.interactive._get_input_loop","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user.","title":"_get_input_loop()"},{"location":"source/#torrentfile.interactive.create_torrent","text":"Create new torrent file interactively.","title":"create_torrent()"},{"location":"source/#torrentfile.interactive.edit_action","text":"Edit the editable values of the torrent meta file.","title":"edit_action()"},{"location":"source/#torrentfile.interactive.get_input","text":"Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call.","title":"get_input()"},{"location":"source/#torrentfile.interactive.recheck_torrent","text":"Check torrent download completed percentage.","title":"recheck_torrent()"},{"location":"source/#torrentfile.interactive.select_action","text":"Operate TorrentFile program interactively through terminal.","title":"select_action()"},{"location":"source/#torrentfile.interactive.showcenter","text":"Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required","title":"showcenter()"},{"location":"source/#torrentfile.interactive.showtext","text":"Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required","title":"showtext()"},{"location":"source/#torrentfile.mixins","text":"Collection of classes that can be used as Mixins with other base classes. Classes such as TorrentFile, TorrentFilev2, and all Hasher classes can use the progress bar mixin. And any class is eligible to use the callback mixin.","title":"mixins"},{"location":"source/#torrentfile.mixins.CbMixin","text":"Mixin class to set a callback during hashing procedure.","title":"CbMixin"},{"location":"source/#torrentfile.mixins.CbMixin.set_callback","text":"Assign a callback to the Hashing class. Parameters: Name Type Description Default func function the callback function required","title":"set_callback()"},{"location":"source/#torrentfile.mixins.ProgMixin","text":"Progress bar mixin class. Displays progress of hashing individual files, usefull when hashing really big files.","title":"ProgMixin"},{"location":"source/#torrentfile.mixins.ProgMixin--methods","text":"prog_start prog_update prog_close","title":"Methods"},{"location":"source/#torrentfile.mixins.ProgMixin.is_active","text":"Test to see if there is an active progress bar for object. Returns: Name Type Description bool True if there is, otherwise False.","title":"is_active()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_close","text":"Finalize the last bits of progress bar. Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one.","title":"prog_close()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_start","text":"Generate a new progress bar for the given file path. Parameters: Name Type Description Default total int the total amount of units accumulating towards. required path str path to file being hashed. required length int the number of characters of the actual progress bar. 50 unit str the text representation of the value being measured. None","title":"prog_start()"},{"location":"source/#torrentfile.mixins.ProgMixin.prog_update","text":"Update progress bar with given amount of progress. Parameters: Name Type Description Default val int the number of bytes count the progress bar should increase. required","title":"prog_update()"},{"location":"source/#torrentfile.mixins.ProgressBar","text":"Holds the state and details of the terminal progress bars. Parameters: Name Type Description Default total int the total amount to be accumulated. required title str the subject of the progress tracker required length int the width of the progress bar required unit str the text representation incremented required","title":"ProgressBar"},{"location":"source/#torrentfile.mixins.ProgressBar.__init__","text":"Construct the progress bar object and store state of it's properties.","title":"__init__()"},{"location":"source/#torrentfile.mixins.ProgressBar.increment","text":"Increase the state of the progress bar value. Parameters: Name Type Description Default value int the amount to increment the state by. required","title":"increment()"},{"location":"source/#torrentfile.mixins.ProgressBar.pbar","text":"Return the size of the filled portion of the progress bar. Returns: Name Type Description str the progress bar characters","title":"pbar()"},{"location":"source/#torrentfile.mixins.waiting","text":"Show loading message while thread completes processing. Parameters: Name Type Description Default msg str Message string printed before the progress bar required flag list Once flag is filled exit loop required timeout int max amount of time to run the function. 180","title":"waiting()"},{"location":"source/#torrentfile.recheck","text":"Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole.","title":"recheck"},{"location":"source/#torrentfile.recheck.Checker","text":"Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required","title":"Checker"},{"location":"source/#torrentfile.recheck.Checker--example","text":">> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location)","title":"Example"},{"location":"source/#torrentfile.recheck.Checker.__init__","text":"Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required","title":"__init__()"},{"location":"source/#torrentfile.recheck.Checker.check_paths","text":"Gather all file paths described in the torrent file.","title":"check_paths()"},{"location":"source/#torrentfile.recheck.Checker.find_root","text":"Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content","title":"find_root()"},{"location":"source/#torrentfile.recheck.Checker.iter_hashes","text":"Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece","title":"iter_hashes()"},{"location":"source/#torrentfile.recheck.Checker.log_msg","text":"Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO","title":"log_msg()"},{"location":"source/#torrentfile.recheck.Checker.piece_checker","text":"Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher.","title":"piece_checker()"},{"location":"source/#torrentfile.recheck.Checker.register_callback","text":"Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required","title":"register_callback()"},{"location":"source/#torrentfile.recheck.Checker.results","text":"Generate result percentage and store for future calls.","title":"results()"},{"location":"source/#torrentfile.recheck.Checker.walk_file_tree","text":"Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required","title":"walk_file_tree()"},{"location":"source/#torrentfile.recheck.FeedChecker","text":"Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required","title":"FeedChecker"},{"location":"source/#torrentfile.recheck.FeedChecker.__init__","text":"Generate hashes of piece length data from filelist contents.","title":"__init__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__iter__","text":"Assign iterator and return self.","title":"__iter__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__next__","text":"Yield back result of comparison.","title":"__next__()"},{"location":"source/#torrentfile.recheck.FeedChecker._gen_padding","text":"Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros.","title":"_gen_padding()"},{"location":"source/#torrentfile.recheck.FeedChecker.extract","text":"Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents.","title":"extract()"},{"location":"source/#torrentfile.recheck.FeedChecker.iter_pieces","text":"Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data.","title":"iter_pieces()"},{"location":"source/#torrentfile.recheck.HashChecker","text":"Bases: ProgMixin Iterate through contents of meta data and verify with file contents. Parameters: Name Type Description Default checker Checker the checker instance that maintains variables. required","title":"HashChecker"},{"location":"source/#torrentfile.recheck.HashChecker.Padder","text":"Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required","title":"Padder"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__init__","text":"Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__iter__","text":"Return self to correctly implement iterator type.","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__next__","text":"Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.__init__","text":"Construct a HybridChecker instance.","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.__iter__","text":"Assign iterator and return self.","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.__next__","text":"Provide the result of comparison.","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.advance","text":"Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size","title":"advance()"},{"location":"source/#torrentfile.recheck.HashChecker.next_file","text":"Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found","title":"next_file()"},{"location":"source/#torrentfile.recheck.HashChecker.process_current","text":"Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration","title":"process_current()"},{"location":"source/#torrentfile.torrent","text":"Classes and procedures pertaining to the creation of torrent meta files.","title":"torrent"},{"location":"source/#torrentfile.torrent--classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#torrentfile.torrent--constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#torrentfile.torrent--bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#torrentfile.torrent--meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#torrentfile.torrent--bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#torrentfile.torrent--v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory.","title":"v1 meta-dictionary"},{"location":"source/#torrentfile.torrent.MetaFile","text":"Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None","title":"MetaFile"},{"location":"source/#torrentfile.torrent.MetaFile.__init__","text":"Construct MetaFile superclass and assign local attributes.","title":"__init__()"},{"location":"source/#torrentfile.torrent.MetaFile.assemble","text":"Overload in subclasses. Raises: Type Description Exception NotImplementedError","title":"assemble()"},{"location":"source/#torrentfile.torrent.MetaFile.set_callback","text":"Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required","title":"set_callback()"},{"location":"source/#torrentfile.torrent.MetaFile.sort_meta","text":"Sort the info and meta dictionaries.","title":"sort_meta()"},{"location":"source/#torrentfile.torrent.MetaFile.write","text":"Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information.","title":"write()"},{"location":"source/#torrentfile.torrent.TorrentAssembler","text":"Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {}","title":"TorrentAssembler"},{"location":"source/#torrentfile.torrent.TorrentAssembler.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles.","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentAssembler._traverse","text":"Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentAssembler.assemble","text":"Assemble the parts of the torrentfile into meta dictionary.","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFile","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {}","title":"TorrentFile"},{"location":"source/#torrentfile.torrent.TorrentFile.__init__","text":"Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {}","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFile.assemble","text":"Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid","text":"Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {}","title":"TorrentFileHybrid"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles.","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid._traverse","text":"Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. DEPRECATED","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileV2","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {}","title":"TorrentFileV2"},{"location":"source/#torrentfile.torrent.TorrentFileV2.__init__","text":"Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {}","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileV2._traverse","text":"Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileV2.assemble","text":"Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent.","title":"assemble()"},{"location":"source/#torrentfile.utils","text":"Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field.","title":"utils"},{"location":"source/#torrentfile.utils.Memo","text":"Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required","title":"Memo"},{"location":"source/#torrentfile.utils.Memo.__call__","text":"Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path.","title":"__call__()"},{"location":"source/#torrentfile.utils.Memo.__init__","text":"Construct for memoization.","title":"__init__()"},{"location":"source/#torrentfile.utils.MissingPathError","text":"Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None","title":"MissingPathError"},{"location":"source/#torrentfile.utils.MissingPathError.__init__","text":"Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class.","title":"__init__()"},{"location":"source/#torrentfile.utils.PieceLengthValueError","text":"Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None","title":"PieceLengthValueError"},{"location":"source/#torrentfile.utils.PieceLengthValueError.__init__","text":"Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class.","title":"__init__()"},{"location":"source/#torrentfile.utils._filelist_total","text":"Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree.","title":"_filelist_total()"},{"location":"source/#torrentfile.utils.filelist_total","text":"Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found.","title":"filelist_total()"},{"location":"source/#torrentfile.utils.get_file_list","text":"Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths.","title":"get_file_list()"},{"location":"source/#torrentfile.utils.get_piece_length","text":"Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length.","title":"get_piece_length()"},{"location":"source/#torrentfile.utils.humanize_bytes","text":"Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes.","title":"humanize_bytes()"},{"location":"source/#torrentfile.utils.next_power_2","text":"Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2.","title":"next_power_2()"},{"location":"source/#torrentfile.utils.normalize_piece_length","text":"Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value.","title":"normalize_piece_length()"},{"location":"source/#torrentfile.utils.path_piece_length","text":"Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content.","title":"path_piece_length()"},{"location":"source/#torrentfile.utils.path_size","text":"Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files.","title":"path_size()"},{"location":"source/#torrentfile.utils.path_stat","text":"Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents.","title":"path_stat()"},{"location":"source/#torrentfile.version","text":"Holds the release version number. Command Line Interface for TorrentFile project. This module provides the primary command line argument parser for the torrentfile package. The main_script function is automatically invoked when called from command line, and parses accompanying arguments. Functions: main_script: process command line arguments and run program. activate_logger: turns on debug mode and logging facility.","title":"version"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter","text":"Bases: HelpFormatter Formatting class for help tips provided by the CLI. Subclasses Argparse.HelpFormatter.","title":"TorrentFileHelpFormatter"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.__init__","text":"Construct HelpFormat class for usage output. Parameters: Name Type Description Default prog str Name of the program. required width int Max width of help message output. 40 max_help_positions int max length until line wrap. 30","title":"__init__()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._format_text","text":"Format text for cli usage messages. Parameters: Name Type Description Default text str Pre-formatted text. required Returns: Type Description str Formatted text from input.","title":"_format_text()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._join_parts","text":"Combine different sections of the help message. Parameters: Name Type Description Default part_strings list List of argument help messages and headers. required Returns: Type Description str Fully formatted help message for CLI.","title":"_join_parts()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter._split_lines","text":"Split multiline help messages and remove indentation. Parameters: Name Type Description Default text str text that needs to be split required _ int max width for line. required","title":"_split_lines()"},{"location":"source/#torrentfile.cli.TorrentFileHelpFormatter.format_headers","text":"Format help message section headers. Parameters: Name Type Description Default parts list List of individual lines for help message. required Returns: Type Description list Input list with formatted section headers.","title":"format_headers()"},{"location":"source/#torrentfile.cli.activate_logger","text":"Activate the builtin logging mechanism when passed debug flag from CLI.","title":"activate_logger()"},{"location":"source/#torrentfile.cli.execute","text":"Initialize Command Line Interface for torrentfile. Parameters: Name Type Description Default args list Commandline arguments. default=None None Returns: Type Description list Depends on what the command line args were.","title":"execute()"},{"location":"source/#torrentfile.cli.main","text":"Initiate main function for CLI script. Edit torrent module. Provides a facility by which certain properties of a torrent meta file can be edited by the user. The various command line arguments indicate which fields should be edited, and what the new value should be. Depending on what fields are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.","title":"main()"},{"location":"source/#torrentfile.edit--keywords","text":"private comment source trackers web-seeds","title":"Keywords"},{"location":"source/#torrentfile.edit.edit_torrent","text":"Edit the properties and values in a torrent meta file. Parameters: Name Type Description Default metafile str path to the torrent meta file. required args dict key value pairs of the properties to be edited. required Returns: Type Description dict The edited and nested Meta and info dictionaries.","title":"edit_torrent()"},{"location":"source/#torrentfile.edit.filter_empty","text":"Remove dictionary keys with empty values. Parameters: Name Type Description Default args dict Editable metafile properties from user. required meta dict Metafile data dictionary. required info dict Metafile info dictionary. required Piece/File Hashers for Bittorrent meta file contents.","title":"filter_empty()"},{"location":"source/#torrentfile.hasher.FileHasher","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True","title":"FileHasher"},{"location":"source/#torrentfile.hasher.FileHasher.__init__","text":"Construct Hasher class instances for each file in torrent.","title":"__init__()"},{"location":"source/#torrentfile.hasher.FileHasher.__iter__","text":"Return self : needed to implement iterator implementation.","title":"__iter__()"},{"location":"source/#torrentfile.hasher.FileHasher.__next__","text":"Calculate layer hashes for contents of file. Returns: Type Description bytes The layer merckle root hash.","title":"__next__()"},{"location":"source/#torrentfile.hasher.FileHasher._calculate_root","text":"Calculate the root hash for opened file.","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.FileHasher._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree.","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.Hasher","text":"Bases: CbMixin , ProgMixin Piece hasher for Bittorrent V1 files. Takes a sorted list of all file paths, calculates sha1 hash for fixed size pieces of file data from each file seemlessly until the last piece which may be smaller than others. Parameters: Name Type Description Default paths list List of files. required piece_length int Size of chuncks to split the data into. required progress int default = None True","title":"Hasher"},{"location":"source/#torrentfile.hasher.Hasher.__init__","text":"Generate hashes of piece length data from filelist contents.","title":"__init__()"},{"location":"source/#torrentfile.hasher.Hasher.__iter__","text":"Iterate through feed pieces. Returns: Name Type Description self iterator Iterator for leaves/hash pieces.","title":"__iter__()"},{"location":"source/#torrentfile.hasher.Hasher.__next__","text":"Generate piece-length pieces of data from input file list. Returns: Type Description bytes SHA1 hash of the piece extracted.","title":"__next__()"},{"location":"source/#torrentfile.hasher.Hasher._handle_partial","text":"Define the handling partial pieces that span 2 or more files. Parameters: Name Type Description Default arr bytearray Incomplete piece containing partial data required Returns: Name Type Description digest bytearray SHA1 digest of the complete piece.","title":"_handle_partial()"},{"location":"source/#torrentfile.hasher.Hasher.next_file","text":"Seemlessly transition to next file in file list. Returns: Name Type Description bool bool True if there is a next file otherwise False.","title":"next_file()"},{"location":"source/#torrentfile.hasher.HasherHybrid","text":"Bases: CbMixin , ProgMixin Calculate root and piece hashes for creating hybrid torrent file. DEPRECATED Create merkle tree layers from sha256 hashed 16KiB blocks of contents. With a branching factor of 2, merge layer hashes until blocks equal piece_length bytes for the piece layer, and then the root hash. Parameters: Name Type Description Default path str path to target file. required piece_length int piece length for data chunks. required progress int default = None True","title":"HasherHybrid"},{"location":"source/#torrentfile.hasher.HasherHybrid.__init__","text":"Construct Hasher class instances for each file in torrent. DEPRECATED","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherHybrid._calculate_root","text":"Calculate the root hash for opened file. DEPRECATED","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherHybrid._pad_remaining","text":"Generate Hash sized, 0 filled bytes for padding. DEPRECATED Parameters: Name Type Description Default block_count int current total number of blocks collected. required Returns: Name Type Description padding bytes Padding to fill remaining portion of tree.","title":"_pad_remaining()"},{"location":"source/#torrentfile.hasher.HasherHybrid.process_file","text":"Calculate layer hashes for contents of file. DEPRECATED Parameters: Name Type Description Default data BytesIO File opened in read mode. required","title":"process_file()"},{"location":"source/#torrentfile.hasher.HasherV2","text":"Bases: CbMixin , ProgMixin Calculate the root hash and piece layers for file contents. DEPRECATED Iterates over 16KiB blocks of data from given file, hashes the data, then creates a hash tree from the individual block hashes until size of hashed data equals the piece-length. Then continues the hash tree until root hash is calculated. Parameters: Name Type Description Default path str Path to file. required piece_length int Size of layer hashes pieces. required progress int default = None True","title":"HasherV2"},{"location":"source/#torrentfile.hasher.HasherV2.__init__","text":"Calculate and store hash information for specific file. DEPRECATED","title":"__init__()"},{"location":"source/#torrentfile.hasher.HasherV2._calculate_root","text":"Calculate root hash for the target file. DEPRECATED","title":"_calculate_root()"},{"location":"source/#torrentfile.hasher.HasherV2.process_file","text":"Calculate hashes over 16KiB chuncks of file content. DEPRECATED Parameters: Name Type Description Default fd TextIOWrapper Opened file in read mode. required","title":"process_file()"},{"location":"source/#torrentfile.hasher.merkle_root","text":"Calculate the merkle root for a seq of sha256 hash digests. Parameters: Name Type Description Default blocks list a sequence of sha256 layer hashes. required Returns: Type Description bytes the sha256 root hash of the merkle tree. Module contains the procedures used for Interactive Mode.","title":"merkle_root()"},{"location":"source/#torrentfile.interactive.InteractiveCreator","text":"Class namespace for interactive program options.","title":"InteractiveCreator"},{"location":"source/#torrentfile.interactive.InteractiveCreator.__init__","text":"Initialize interactive meta file creator dialog.","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveCreator.get_props","text":"Gather details for torrentfile from user.","title":"get_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor","text":"Interactive dialog class for torrent editing.","title":"InteractiveEditor"},{"location":"source/#torrentfile.interactive.InteractiveEditor.__init__","text":"Initialize the Interactive torrent editor guide. Parameters: Name Type Description Default metafile str user input string identifying the path to a torrent meta file. required","title":"__init__()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.edit_props","text":"Loop continuosly for edits until user signals DONE.","title":"edit_props()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.sanatize_response","text":"Convert the input data into a form recognizable by the program. Parameters: Name Type Description Default key str name of the property and attribute being eddited. required response str User input value the property is being edited to. required","title":"sanatize_response()"},{"location":"source/#torrentfile.interactive.InteractiveEditor.show_current","text":"Display the current met file information to screen.","title":"show_current()"},{"location":"source/#torrentfile.interactive._get_input","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required Returns: Type Description str The text input received from the user.","title":"_get_input()"},{"location":"source/#torrentfile.interactive._get_input_loop","text":"Gather information needed from user. Parameters: Name Type Description Default txt str The message usually containing instructions for the user. required func function Validate/Check user input data, failure = retry, success = continue. required Returns: Type Description str The text input received from the user.","title":"_get_input_loop()"},{"location":"source/#torrentfile.interactive.create_torrent","text":"Create new torrent file interactively.","title":"create_torrent()"},{"location":"source/#torrentfile.interactive.edit_action","text":"Edit the editable values of the torrent meta file.","title":"edit_action()"},{"location":"source/#torrentfile.interactive.get_input","text":"Determine appropriate input function to call. Parameters: Name Type Description Default *args tuple Arbitrary number of args to pass to next function () Returns: Type Description str The results of the function call.","title":"get_input()"},{"location":"source/#torrentfile.interactive.recheck_torrent","text":"Check torrent download completed percentage.","title":"recheck_torrent()"},{"location":"source/#torrentfile.interactive.select_action","text":"Operate TorrentFile program interactively through terminal.","title":"select_action()"},{"location":"source/#torrentfile.interactive.showcenter","text":"Print text to screen in the center position of the terminal. Parameters: Name Type Description Default txt str the preformated message to send to stdout. required","title":"showcenter()"},{"location":"source/#torrentfile.interactive.showtext","text":"Print contents of txt to screen. Parameters: Name Type Description Default txt str text to print to terminal. required Module container Checker Class. The CheckerClass takes a torrentfile and tha path to it's contents. It will then iterate through every file and directory contained and compare their data to values contained within the torrent file. Completion percentages will be printed to screen for each file and at the end for the torrentfile as a whole.","title":"showtext()"},{"location":"source/#torrentfile.recheck.Checker","text":"Bases: ProgMixin Check a given file or directory to see if it matches a torrentfile. Public constructor for Checker class instance. Parameters: Name Type Description Default metafile str Path to \".torrent\" file. required path str Path where the content is located in filesystem. required","title":"Checker"},{"location":"source/#torrentfile.recheck.Checker--example","text":">> metafile = \"/path/to/torrentfile/content_file_or_dir.torrent\" >> location = \"/path/to/location\" >> os.path.exists(\"/path/to/location/content_file_or_dir\") Out: True >> checker = Checker(metafile, location)","title":"Example"},{"location":"source/#torrentfile.recheck.Checker.__init__","text":"Validate data against hashes contained in .torrent file. Parameters: Name Type Description Default metafile str path to .torrent file required path str path to content or contents parent directory. required","title":"__init__()"},{"location":"source/#torrentfile.recheck.Checker.check_paths","text":"Gather all file paths described in the torrent file.","title":"check_paths()"},{"location":"source/#torrentfile.recheck.Checker.find_root","text":"Check path for torrent content. The path can be a relative or absolute filesystem path. In the case where the content is a single file, the path may point directly to the the file, or it may point to the parent directory. If content points to a directory. The directory will be checked to see if it matches the torrent's name, if not the directories contents will be searched. The returned value will be the absolute path that matches the torrent's name. Parameters: Name Type Description Default path str root path to torrent content required Returns: Type Description str root path to content","title":"find_root()"},{"location":"source/#torrentfile.recheck.Checker.iter_hashes","text":"Produce results of comparing torrent contents piece by piece. Yields: Name Type Description chunck bytes hash of data found on disk piece bytes hash of data when complete and correct path str path to file being hashed size int length of bytes hashed for piece","title":"iter_hashes()"},{"location":"source/#torrentfile.recheck.Checker.log_msg","text":"Log message msg to logger and send msg to callback hook. Parameters: Name Type Description Default *args dict formatting args for log message () level int Log level for this message; default= logging.INFO logging.INFO","title":"log_msg()"},{"location":"source/#torrentfile.recheck.Checker.piece_checker","text":"Check individual pieces of the torrent. Returns: Type Description HashChecker | FeedChecker Individual piece hasher.","title":"piece_checker()"},{"location":"source/#torrentfile.recheck.Checker.register_callback","text":"Register hooks from 3rd party programs to access generated info. Parameters: Name Type Description Default hook function callback function for the logging feature. required","title":"register_callback()"},{"location":"source/#torrentfile.recheck.Checker.results","text":"Generate result percentage and store for future calls.","title":"results()"},{"location":"source/#torrentfile.recheck.Checker.walk_file_tree","text":"Traverse File Tree dictionary to get file details. Extract full pathnames, length, root hash, and layer hashes for each file included in the .torrent's file tree. Parameters: Name Type Description Default tree dict File Tree dict extracted from torrent file. required partials list list of intermediate pathnames. required","title":"walk_file_tree()"},{"location":"source/#torrentfile.recheck.FeedChecker","text":"Bases: ProgMixin Validates torrent content. Seemlesly validate torrent file contents by comparing hashes in metafile against data on disk. Parameters: Name Type Description Default checker object the checker class instance. required","title":"FeedChecker"},{"location":"source/#torrentfile.recheck.FeedChecker.__init__","text":"Generate hashes of piece length data from filelist contents.","title":"__init__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__iter__","text":"Assign iterator and return self.","title":"__iter__()"},{"location":"source/#torrentfile.recheck.FeedChecker.__next__","text":"Yield back result of comparison.","title":"__next__()"},{"location":"source/#torrentfile.recheck.FeedChecker._gen_padding","text":"Create padded pieces where file sizes do not match. Parameters: Name Type Description Default partial bytes any remaining data from last file processed. required length int size of space that needs padding required read int portion of length already padded 0 Yields: Type Description bytes A piece length sized block of zeros.","title":"_gen_padding()"},{"location":"source/#torrentfile.recheck.FeedChecker.extract","text":"Split file paths contents into blocks of data for hash pieces. Parameters: Name Type Description Default path str path to content. required partial bytes any remaining content from last file. required Returns: Type Description bytearray Hash digest for block of .torrent contents.","title":"extract()"},{"location":"source/#torrentfile.recheck.FeedChecker.iter_pieces","text":"Iterate through, and hash pieces of torrent contents. Yields: Name Type Description piece bytes hash digest for block of torrent data.","title":"iter_pieces()"},{"location":"source/#torrentfile.recheck.HashChecker","text":"Bases: ProgMixin Iterate through contents of meta data and verify with file contents. Parameters: Name Type Description Default checker Checker the checker instance that maintains variables. required","title":"HashChecker"},{"location":"source/#torrentfile.recheck.HashChecker.Padder","text":"Padding class to generate padding hashes wherever needed. Parameters: Name Type Description Default length the total size of the mock file generating padding for. required piece_length int the block size that each hash represents. required","title":"Padder"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__init__","text":"Construct padding class to Mock missing or incomplete files. Parameters: Name Type Description Default length int size of the file required piece_length int the piece length for each iteration. required","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__iter__","text":"Return self to correctly implement iterator type.","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.Padder.__next__","text":"Iterate through seemingly endless sha256 hashes of zeros. Returns: Name Type Description tuple bytes returns the padding Raises: Type Description StopIteration","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.__init__","text":"Construct a HybridChecker instance.","title":"__init__()"},{"location":"source/#torrentfile.recheck.HashChecker.__iter__","text":"Assign iterator and return self.","title":"__iter__()"},{"location":"source/#torrentfile.recheck.HashChecker.__next__","text":"Provide the result of comparison.","title":"__next__()"},{"location":"source/#torrentfile.recheck.HashChecker.advance","text":"Increment the number of pieces processed for the current file. Returns: Type Description tuple the piece and size","title":"advance()"},{"location":"source/#torrentfile.recheck.HashChecker.next_file","text":"Remove all references to processed files and prepare for the next. Returns: Type Description bool if there is a next file found","title":"next_file()"},{"location":"source/#torrentfile.recheck.HashChecker.process_current","text":"Gather necessary information to compare to metafile details. Returns: Type Description tuple a tuple containing the layer, piece, current path and size Raises: Type Description StopIteration Classes and procedures pertaining to the creation of torrent meta files.","title":"process_current()"},{"location":"source/#torrentfile.torrent--classes","text":"TorrentFile construct .torrent file. TorrentFileV2 construct .torrent v2 files using provided data. MetaFile base class for all MetaFile classes.","title":"Classes"},{"location":"source/#torrentfile.torrent--constants","text":"BLOCK_SIZE : int size of leaf hashes for merkle tree. HASH_SIZE : int Length of a sha256 hash.","title":"Constants"},{"location":"source/#torrentfile.torrent--bittorrent-v2","text":"From Bittorrent.org Documentation pages. Implementation details for Bittorrent Protocol v2. Note All strings in a .torrent file that contain text must be UTF-8 encoded.","title":"Bittorrent V2"},{"location":"source/#torrentfile.torrent--meta-version-2-dictionary","text":"\"announce\": The URL of the tracker. \"info\": This maps to a dictionary, with keys described below. \"name\": A display name for the torrent. It is purely advisory. \"piece length\": The number of bytes that each logical piece in the peer protocol refers to. I.e. it sets the granularity of piece, request, bitfield and have messages. It must be a power of two and at least 6KiB. \"meta version\": An integer value, set to 2 to indicate compatibility with the current revision of this specification. Version 1 is not assigned to avoid confusion with BEP3. Future revisions will only increment this issue to indicate an incompatible change has been made, for example that hash algorithms were changed due to newly discovered vulnerabilities. Lementations must check this field first and indicate that a torrent is of a newer version than they can handle before performing other idations which may result in more general messages about invalid files. Files are mapped into this piece address space so that each non-empty \"file tree\": A tree of dictionaries where dictionary keys represent UTF-8 encoded path elements. Entries with zero-length keys describe the properties of the composed path at that point. 'UTF-8 encoded' context only means that if the native encoding is known at creation time it must be converted to UTF-8. Keys may contain invalid UTF-8 sequences or characters and names that are reserved on specific filesystems. Implementations must be prepared to sanitize them. On platforms path components exactly matching '.' and '..' must be sanitized since they could lead to directory traversal attacks and conflicting path descriptions. On platforms that require UTF-8 path components this sanitizing step must happen after normalizing overlong UTF-8 encodings. File is aligned to a piece boundary and occurs in same order as the file tree. The last piece of each file may be shorter than the specified piece length, resulting in an alignment gap. \"length\": Length of the file in bytes. Presence of this field indicates that the dictionary describes a file, not a directory. Which means it must not have any sibling entries. \"pieces root\": For non-empty files this is the the root hash of a merkle tree with a branching factor of 2, constructed from 16KiB blocks of the file. The last block may be shorter than 16KiB. The remaining leaf hashes beyond the end of the file required to construct upper layers of the merkle tree are set to zero. As of meta version 2 SHA2-256 is used as digest function for the merkle tree. The hash is stored in its binary form, not as human-readable string. \"piece layers\": A dictionary of strings. For each file in the file tree that is larger than the piece size it contains one string value. The keys are the merkle roots while the values consist of concatenated hashes of one layer within that merkle tree. The layer is chosen so that one hash covers piece length bytes. For example if the piece size is 16KiB then the leaf hashes are used. If a piece size of 128KiB is used then 3rd layer up from the leaf hashes is used. Layer hashes which exclusively cover data beyond the end of file, i.e. are only needed to balance the tree, are omitted. All hashes are stored in their binary format. A torrent is not valid if this field is absent, the contained hashes do not match the merkle roots or are not from the correct layer. Important The file tree root dictionary itself must not be a file, i.e. it must not contain a zero-length key with a dictionary containing a length key.","title":"Meta Version 2 Dictionary:"},{"location":"source/#torrentfile.torrent--bittorrent-v1","text":"","title":"Bittorrent V1"},{"location":"source/#torrentfile.torrent--v1-meta-dictionary","text":"announce: The URL of the tracker. info: This maps to a dictionary, with keys described below. name : maps to a UTF-8 encoded string which is the suggested name to save the file (or directory) as. It is purely advisory. piece length : maps to the number of bytes in each piece the file is split into. For the purposes of transfer, files are split into fixed-size pieces which are all the same length except for possibly the last one which may be truncated. piece length : is almost always a power of two, most commonly 2^18 = 256 K pieces : maps to a string whose length is a multiple of 20. It is to be subdivided into strings of length 20, each of which is the SHA1 hash of the piece at the corresponding index. length : In the single file case, maps to the length of the file in bytes. files : If present then the download represents a single file, otherwise it represents a set of files which go in a directory structure. For the purposes of the other keys, the multi-file case is treated as only having a single file by concatenating the files in the order they appear in the files list. The files list is the value files maps to, and is a list of dictionaries containing the following keys: path : A list of UTF-8 encoded strings corresponding to subdirectory names, the last of which is the actual file name length : Maps to the length of the file in bytes. length : Only present if the content is a single file. Maps to the length of the file in bytes. Note In the single file case, the name key is the name of a file, in the muliple file case, it's the name of a directory.","title":"v1 meta-dictionary"},{"location":"source/#torrentfile.torrent.MetaFile","text":"Base Class for all TorrentFile classes. Parameters: Name Type Description Default path str target path to torrent content. Default: None None announce str One or more tracker URL's. Default: None None comment str A comment. Default: None None piece_length int Size of torrent pieces. Default: None None private bool For private trackers. Default: None False outfile str target path to write .torrent file. Default: None None source str Private tracker source. Default: None None progress str level of progress bar displayed Default: \"1\" 1 cwd bool If True change default save location to current directory False httpseeds list one or more web addresses where torrent content can be found. None url_list list one or more web addressess where torrent content exists. None content str alias for 'path' arg. None meta_version int indicates which Bittorrent protocol to use for hashing content None","title":"MetaFile"},{"location":"source/#torrentfile.torrent.MetaFile.__init__","text":"Construct MetaFile superclass and assign local attributes.","title":"__init__()"},{"location":"source/#torrentfile.torrent.MetaFile.assemble","text":"Overload in subclasses. Raises: Type Description Exception NotImplementedError","title":"assemble()"},{"location":"source/#torrentfile.torrent.MetaFile.set_callback","text":"Assign a callback function for the Hashing class to call for each hash. Parameters: Name Type Description Default func function The callback function which accepts a single paramter. required","title":"set_callback()"},{"location":"source/#torrentfile.torrent.MetaFile.sort_meta","text":"Sort the info and meta dictionaries.","title":"sort_meta()"},{"location":"source/#torrentfile.torrent.MetaFile.write","text":"Write meta information to .torrent file. Parameters: Name Type Description Default outfile str Destination path for .torrent file. default=None None Returns: Name Type Description outfile str Where the .torrent file was writen. meta dict .torrent meta information.","title":"write()"},{"location":"source/#torrentfile.torrent.TorrentAssembler","text":"Bases: MetaFile Assembler class for Bittorrent version 2 and hybrid meta files. This differs from the TorrentFileV2 and TorrentFileHybrid, because it can be used as an iterator and works for both versions. Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {}","title":"TorrentAssembler"},{"location":"source/#torrentfile.torrent.TorrentAssembler.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles.","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentAssembler._traverse","text":"Build meta dictionary while walking directory. Parameters: Name Type Description Default path str Path to target file. required","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentAssembler.assemble","text":"Assemble the parts of the torrentfile into meta dictionary.","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFile","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta files. Construct Torrentfile class instance object. Parameters: Name Type Description Default **kwargs dict Dictionary containing torrent file options. {}","title":"TorrentFile"},{"location":"source/#torrentfile.torrent.TorrentFile.__init__","text":"Construct TorrentFile instance with given keyword args. Parameters: Name Type Description Default **kwargs dict dictionary of keyword args passed to superclass. {}","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFile.assemble","text":"Assemble components of torrent metafile. Returns: Type Description dict metadata dictionary for torrent file","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid","text":"Bases: MetaFile , ProgMixin Construct the Hybrid torrent meta file with provided parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent options. {}","title":"TorrentFileHybrid"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.__init__","text":"Create Bittorrent v1 v2 hybrid metafiles.","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid._traverse","text":"Build meta dictionary while walking directory. DEPRECATED Parameters: Name Type Description Default path str Path to target file. required","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileHybrid.assemble","text":"Assemble the parts of the torrentfile into meta dictionary. DEPRECATED","title":"assemble()"},{"location":"source/#torrentfile.torrent.TorrentFileV2","text":"Bases: MetaFile , ProgMixin Class for creating Bittorrent meta v2 files. DEPRECATED Parameters: Name Type Description Default **kwargs dict Keyword arguments for torrent file options. {}","title":"TorrentFileV2"},{"location":"source/#torrentfile.torrent.TorrentFileV2.__init__","text":"Construct TorrentFileV2 Class instance from given parameters. DEPRECATED Parameters: Name Type Description Default **kwargs dict keywword arguments to pass to superclass. {}","title":"__init__()"},{"location":"source/#torrentfile.torrent.TorrentFileV2._traverse","text":"Walk directory tree. DEPRECATED Parameters: Name Type Description Default path str Path to file or directory. required","title":"_traverse()"},{"location":"source/#torrentfile.torrent.TorrentFileV2.assemble","text":"Assemble then return the meta dictionary for encoding. DEPRECATED Returns: Name Type Description meta dict Metainformation about the torrent. Utility functions and classes used throughout package. Functions: get_piece_length: calculate ideal piece length for torrent file. sortfiles: traverse directory in sorted order yielding paths encountered. path_size: Sum the sizes of each file in path. get_file_list: Return list of all files contained in directory. path_stat: Get ideal piece length, total size, and file list for directory. path_piece_length: Get ideal piece length based on size of directory. Classes: MissingPathError: Custom exception raised when no path was provided to CLI. PieceLengthValueError: Custom exception raised when incorrect input value used for piece length field.","title":"assemble()"},{"location":"source/#torrentfile.utils.Memo","text":"Memoice chache object. Parameters: Name Type Description Default func function The function that is being memoized. required","title":"Memo"},{"location":"source/#torrentfile.utils.Memo.__call__","text":"Invoke each time memo function is called. Parameters: Name Type Description Default path str The relative or absolute path being used as key in cache dict. required Returns: Name Type Description Any The results of calling the function with path.","title":"__call__()"},{"location":"source/#torrentfile.utils.Memo.__init__","text":"Construct for memoization.","title":"__init__()"},{"location":"source/#torrentfile.utils.MissingPathError","text":"Bases: Exception Path parameter is required to specify target content. Creating a .torrent file with no contents seems rather silly. Parameters: Name Type Description Default message str Message for user (optional). None","title":"MissingPathError"},{"location":"source/#torrentfile.utils.MissingPathError.__init__","text":"Raise when creating a meta file without specifying target content. The message argument is a message to pass to Exception base class.","title":"__init__()"},{"location":"source/#torrentfile.utils.PieceLengthValueError","text":"Bases: Exception Piece Length parameter must equal a perfect power of 2. Parameters: Name Type Description Default message str Message for user (optional). None","title":"PieceLengthValueError"},{"location":"source/#torrentfile.utils.PieceLengthValueError.__init__","text":"Raise when creating a meta file with incorrect piece length value. The message argument is a message to pass to Exception base class.","title":"__init__()"},{"location":"source/#torrentfile.utils._filelist_total","text":"Search directory tree for files. Parameters: Name Type Description Default path str Path to file or directory base required Returns: Type Description int Sum of all filesizes in filelist. list All file paths within directory tree.","title":"_filelist_total()"},{"location":"source/#torrentfile.utils.filelist_total","text":"Perform error checking and format conversion to os.PathLike. Parameters: Name Type Description Default pathstring str An existing filesystem path. required Returns: Type Description os . PathLike Input path converted to bytes format. Raises: Type Description MissingPathError File could not be found.","title":"filelist_total()"},{"location":"source/#torrentfile.utils.get_file_list","text":"Return a sorted list of file paths contained in directory. Parameters: Name Type Description Default path str target file or directory. required Returns: Type Description list sorted list of file paths.","title":"get_file_list()"},{"location":"source/#torrentfile.utils.get_piece_length","text":"Calculate the ideal piece length for bittorrent data. Parameters: Name Type Description Default size int Total bits of all files incluided in .torrent file. required Returns: Type Description int Ideal piece length.","title":"get_piece_length()"},{"location":"source/#torrentfile.utils.humanize_bytes","text":"Convert integer into human readable memory sized denomination. Parameters: Name Type Description Default amount int total number of bytes. required Returns: Type Description str human readable representation of the given amount of bytes.","title":"humanize_bytes()"},{"location":"source/#torrentfile.utils.next_power_2","text":"Calculate the next perfect power of 2 equal to or greater than value. Parameters: Name Type Description Default value int integer value that is less than some perfect power of 2. required Returns: Type Description int The next power of 2 greater than value, or value if already power of 2.","title":"next_power_2()"},{"location":"source/#torrentfile.utils.normalize_piece_length","text":"Verify input piece_length is valid and convert accordingly. Parameters: Name Type Description Default piece_length int | str The piece length provided by user. required Returns: Type Description int normalized piece length. Raises: Type Description PieceLengthValueError : If piece length is improper value.","title":"normalize_piece_length()"},{"location":"source/#torrentfile.utils.path_piece_length","text":"Calculate piece length for input path and contents. Parameters: Name Type Description Default path str The absolute path to directory and contents. required Returns: Type Description int The size of pieces of torrent content.","title":"path_piece_length()"},{"location":"source/#torrentfile.utils.path_size","text":"Return the total size of all files in path recursively. Parameters: Name Type Description Default path str path to target file or directory. required Returns: Type Description int total size of files.","title":"path_size()"},{"location":"source/#torrentfile.utils.path_stat","text":"Calculate directory statistics. Parameters: Name Type Description Default path str The path to start calculating from. required Returns: Type Description list List of all files contained in Directory int Total sum of bytes from all contents of dir int The size of pieces of the torrent contents. Holds the release version number.","title":"path_stat()"}]} \ No newline at end of file diff --git a/docs/sitemap.xml.gz b/docs/sitemap.xml.gz index ece310b3bac931a200b8df6562e294181637f706..df8d5c2a8081c7b9ce8d55a0ee6f1dac0e792a88 100644 GIT binary patch delta 15 WcmZo*YG7iQ@8;l`@}+Dd`%eHN?*&x= delta 15 WcmZo*YG7iQ@8;mx`>b>#`%eHOn+0zG diff --git a/docs/source/index.html b/docs/source/index.html index fba17d53..f0e215b7 100644 --- a/docs/source/index.html +++ b/docs/source/index.html @@ -17,7 +17,7 @@ - Source Code - TorrentFile Docs + API - TorrentFile Docs @@ -104,7 +104,7 @@
    - Source Code + API
    @@ -229,7 +229,7 @@
  • - Help Messages + Help
  • @@ -244,7 +244,7 @@
  • - Man Page + Details
  • @@ -298,12 +298,12 @@ - Source Code + API @@ -1051,7 +1051,7 @@

    Recheck Module

  • HashChecker - Verify that root hashes of content files match the .torrent files.
  • + Iterate through contents of meta data and verify with file contents. @@ -1119,15 +1119,7 @@

    ------

    - - -

    - torrentfile - - - -

    - +

    Torrentfile can create Bittorrent metafiles for any content.

    @@ -1154,15 +1146,19 @@

    + + + +
    -

    +

    create(args) -

    +

    @@ -1211,76 +1207,6 @@

    607
    +608
    +609
    +610
    +611
     612
     613
     614
    @@ -30816,11 +30676,7 @@ 

    629 630 631 -632 -633 -634 -635 -636

    def process_current(self) -> tuple:
    +632
    def process_current(self) -> tuple:
         """
         Gather necessary information to compare to metafile details.
     
    @@ -32265,13 +32121,13 @@ 

    kwargs**kwargs dict

    Keyword arguments for torrent options.

    - required + {}
    kwargs**kwargs dict

    Dictionary containing torrent file options.

    - required + {}
    kwargs**kwargs dict

    dictionary of keyword args passed to superclass.

    - required + {}
    kwargs**kwargs dict

    Keyword arguments for torrent options.

    - required + {}
    kwargs**kwargs dict

    Keyword arguments for torrent file options.

    - required + {}
    kwargs**kwargs dict

    keywword arguments to pass to superclass.

    - required + {}
    args*args list

    Filesystem locations for removing.

    - required + ()
    -
    - Source code in torrentfile\commands.py -
    49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    -77
    -78
    -79
    -80
    -81
    def create(args: list):
    -    """
    -    Execute the create CLI sub-command to create a new torrent metafile.
    -
    -    Parameters
    -    ----------
    -    args : argparse.Namespace
    -        positional and optional CLI arguments.
    -
    -    Returns
    -    -------
    -    torrentfile.MetaFile
    -        object containing the path to created metafile and its contents.
    -    """
    -    kwargs = vars(args)
    -    logger.debug("Creating torrent from %s", args.content)
    -    if args.meta_version == "1":
    -        torrent = TorrentFile(**kwargs)
    -    else:
    -        torrent = TorrentAssembler(**kwargs)
    -    outfile, meta = torrent.write()
    -
    -    if args.magnet:
    -        magnet(outfile)
    -
    -    args.torrent = torrent
    -    args.kwargs = kwargs
    -    args.outfile = outfile
    -    args.meta = meta
    -
    -    print("\nTorrent Save Path: ", os.path.abspath(str(outfile)))
    -    logger.debug("Output path: %s", str(outfile))
    -    return args
    -
    -
    -
    @@ -1291,11 +1217,11 @@

    -

    +

    execute(args=None) -

    +
    @@ -1344,716 +1270,6 @@

    -
    - Source code in torrentfile\cli.py -
    168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    -403
    -404
    -405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    -519
    -520
    def execute(args=None) -> list:
    -    """
    -    Initialize Command Line Interface for torrentfile.
    -
    -    Parameters
    -    ----------
    -    args : list
    -        Commandline arguments. default=None
    -
    -    Returns
    -    -------
    -    list
    -        Depends on what the command line args were.
    -    """
    -    if not args:
    -        if sys.argv[1:]:
    -            args = sys.argv[1:]
    -        else:
    -            args = ["-h"]
    -
    -    parser = ArgumentParser(
    -        "torrentfile",
    -        description=(
    -            "Command line tools for creating, editing, checking and "
    -            "interacting with Bittorrent metainfo files"
    -        ),
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -        conflict_handler="resolve",
    -    )
    -
    -    parser.add_argument(
    -        "-i",
    -        "--interactive",
    -        action="store_true",
    -        dest="interactive",
    -        help="select program options interactively",
    -    )
    -
    -    parser.add_argument(
    -        "-V",
    -        "--version",
    -        action="version",
    -        version=f"torrentfile v{version}",
    -        help="show program version and exit",
    -    )
    -
    -    parser.add_argument(
    -        "-v",
    -        "--verbose",
    -        action="store_true",
    -        dest="debug",
    -        help="output debug information",
    -    )
    -
    -    parser.set_defaults(func=parser.print_help)
    -
    -    subparsers = parser.add_subparsers(
    -        title="Actions",
    -        dest="command",
    -        metavar="create, edit, magnet, recheck",
    -    )
    -
    -    create_parser = subparsers.add_parser(
    -        "create",
    -        help="""Generate a new torrent meta file.""",
    -        prefix_chars="-",
    -        aliases=["c", "new"],
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    create_parser.add_argument(
    -        "-a",
    -        "-t",
    -        "--announce",
    -        "--tracker",
    -        action="store",
    -        dest="announce",
    -        metavar="<url>",
    -        nargs="+",
    -        default=[],
    -        help="One or more space-seperated torrent tracker url(s).",
    -    )
    -
    -    create_parser.add_argument(
    -        "-p",
    -        "--private",
    -        action="store_true",
    -        dest="private",
    -        help="Creates private torrent with multi-tracker and DHT turned off.",
    -    )
    -
    -    create_parser.add_argument(
    -        "-s",
    -        "--source",
    -        action="store",
    -        dest="source",
    -        metavar="<source>",
    -        help="Add a source string. Useful for cross-seeding.",
    -    )
    -
    -    create_parser.add_argument(
    -        "-m",
    -        "--magnet",
    -        action="store_true",
    -        dest="magnet",
    -    )
    -
    -    create_parser.add_argument(
    -        "-c",
    -        "--comment",
    -        action="store",
    -        dest="comment",
    -        metavar="<comment>",
    -        help="Include a comment in file metadata",
    -    )
    -
    -    create_parser.add_argument(
    -        "-o",
    -        "--out",
    -        action="store",
    -        dest="outfile",
    -        metavar="<path>",
    -        help="Output save path for created .torrent file",
    -    )
    -
    -    create_parser.add_argument(
    -        "--cwd",
    -        "--current",
    -        action="store_true",
    -        dest="cwd",
    -        help="Save output .torrent file to current directory",
    -    )
    -
    -    create_parser.add_argument(
    -        "--prog",
    -        "--progress",
    -        default="1",
    -        action="store",
    -        dest="progress",
    -        help="""
    -        Set the progress bar level.
    -        Options = 0, 1
    -        (0) = Do not display progress bar.
    -        (1) = Display progress bar.
    -        """,
    -    )
    -
    -    create_parser.add_argument(
    -        "--meta-version",
    -        default="1",
    -        choices=["1", "2", "3"],
    -        action="store",
    -        dest="meta_version",
    -        metavar="<int>",
    -        help="""
    -        Bittorrent metafile version.
    -        Options = 1, 2, 3
    -        (1) = Bittorrent v1 (Default)
    -        (2) = Bittorrent v2
    -        (3) = Bittorrent v1 & v2 hybrid
    -        """,
    -    )
    -
    -    create_parser.add_argument(
    -        "--piece-length",
    -        action="store",
    -        dest="piece_length",
    -        metavar="<int>",
    -        help="""
    -        (Default: <blank>) Number of bytes for per chunk of data transmitted
    -        by Bittorrent client. Acceptable values include integers 14-26 which
    -        will be interpreted as a perfect power of 2.  e.g. 14 = 16KiB pieces.
    -        Examples:: [--piece-length 14] [--piece-length 20]
    -        """,
    -    )
    -
    -    create_parser.add_argument(
    -        "-w",
    -        "--web-seed",
    -        action="store",
    -        dest="url_list",
    -        metavar="<url>",
    -        nargs="+",
    -        help="list of web addresses where torrent data exists (GetRight).",
    -    )
    -
    -    create_parser.add_argument(
    -        "--http-seed",
    -        action="store",
    -        dest="httpseeds",
    -        metavar="<url>",
    -        nargs="+",
    -        help="list of URLs, addresses where content can be found (Hoffman).",
    -    )
    -
    -    create_parser.add_argument(
    -        "content",
    -        action="store",
    -        metavar="<content>",
    -        nargs="?",
    -        help="Path to content file or directory",
    -    )
    -
    -    create_parser.set_defaults(func=create)
    -
    -    edit_parser = subparsers.add_parser(
    -        "edit",
    -        help="""
    -        Edit existing torrent meta file.
    -        """,
    -        aliases=["e"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    edit_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        help="path to *.torrent file",
    -        metavar="<*.torrent>",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--tracker",
    -        action="store",
    -        dest="announce",
    -        metavar="<url>",
    -        nargs="+",
    -        help="""
    -        Replace current list of tracker/announce urls with one or more space
    -        seperated Bittorrent tracker announce url(s).
    -        """,
    -    )
    -
    -    edit_parser.add_argument(
    -        "--web-seed",
    -        action="store",
    -        dest="url_list",
    -        metavar="<url>",
    -        nargs="+",
    -        help="Replace current list of web-seed urls with one or more url(s)",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--http-seed",
    -        action="store",
    -        dest="httpseeds",
    -        metavar="<url>",
    -        nargs="+",
    -        help="replace all currently listed addresses with new list (Hoffman).",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--private",
    -        action="store_true",
    -        help="Make torrent private.",
    -        dest="private",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--comment",
    -        help="Replaces any existing comment with <comment>",
    -        metavar="<comment>",
    -        dest="comment",
    -        action="store",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--source",
    -        action="store",
    -        dest="source",
    -        metavar="<source>",
    -        help="Replaces current source with <source>",
    -    )
    -
    -    edit_parser.set_defaults(func=edit)
    -
    -    magnet_parser = subparsers.add_parser(
    -        "magnet",
    -        help="""
    -        Generate magnet url from an existing Bittorrent meta file.
    -        """,
    -        aliases=["m"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    magnet_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        help="Path to Bittorrent meta file.",
    -        metavar="<*.torrent>",
    -    )
    -
    -    magnet_parser.set_defaults(func=magnet)
    -
    -    check_parser = subparsers.add_parser(
    -        "recheck",
    -        help="""
    -        Calculate amount of torrent meta file's content is found on disk.
    -        """,
    -        aliases=["r", "check"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    check_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        metavar="<*.torrent>",
    -        help="path to .torrent file.",
    -    )
    -
    -    check_parser.add_argument(
    -        "content",
    -        action="store",
    -        metavar="<content>",
    -        help="path to content file or directory",
    -    )
    -
    -    check_parser.set_defaults(func=recheck)
    -
    -    info_parser = subparsers.add_parser(
    -        "info",
    -        help="""
    -        Show detailed information about a torrent file.
    -        """,
    -        aliases=["i"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    info_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        metavar="<*.torrent>",
    -        help="path to pre-existing torrent file.",
    -    )
    -
    -    info_parser.set_defaults(func=info)
    -
    -    args = parser.parse_args(args)
    -
    -    if args.debug:
    -        activate_logger()
    -
    -    if args.interactive:
    -        return select_action()
    -
    -    if hasattr(args, "func"):
    -        return args.func(args)
    -    return args
    -
    -
    -

    @@ -2064,11 +1280,11 @@

    -

    +

    info(args) -

    +
    @@ -2099,80 +1315,6 @@

    -
    - Source code in torrentfile\commands.py -
     84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    def info(args: list):
    -    """
    -    Show torrent metafile details to user via stdout.
    -
    -    Parameters
    -    ----------
    -    args : dict
    -        command line arguements provided by the user.
    -    """
    -    metafile = args.metafile
    -    meta = pyben.load(metafile)
    -    info = meta["info"]
    -    del meta["info"]
    -    meta.update(info)
    -    if "private" in meta and meta["private"] == 1:
    -        meta["private"] = "True"
    -    if "announce-list" in meta:
    -        lst = meta["announce-list"]
    -        meta["announce-list"] = ", ".join([j for i in lst for j in i])
    -    if "url-list" in meta:
    -        meta["url-list"] = ", ".join(meta["url-list"])
    -    if "httpseeds" in meta:
    -        meta["httpseeds"] = ", ".join(meta["httpseeds"])
    -    text = []
    -    longest = max([len(i) for i in meta.keys()])
    -    for key, val in meta.items():
    -        if key not in ["pieces", "piece layers", "files", "file tree"]:
    -            prefix = longest - len(key) + 1
    -            string = key + (" " * prefix) + str(val)
    -            text.append(string)
    -    most = max([len(i) for i in text])
    -    text = ["-" * most, "\n"] + text + ["\n", "-" * most]
    -    output = "\n".join(text)
    -    print(output)
    -    return output
    -
    -
    -
    @@ -2183,11 +1325,11 @@

    -

    +

    magnet(metafile) -

    +
    @@ -2236,94 +1378,6 @@

    -
    - Source code in torrentfile\commands.py -
    175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    def magnet(metafile: str):
    -    """
    -    Create a magnet URI from a Bittorrent meta file.
    -
    -    Parameters
    -    ----------
    -    metafile : (Namespace||str)
    -        Namespace class for CLI arguments.
    -
    -    Returns
    -    -------
    -    str
    -        created magnet URI.
    -    """
    -    if hasattr(metafile, "metafile"):
    -        metafile = metafile.metafile
    -    if not os.path.exists(metafile):
    -        raise FileNotFoundError
    -
    -    meta = pyben.load(metafile)
    -    info = meta["info"]
    -    binfo = pyben.dumps(info)
    -    infohash = sha1(binfo).hexdigest().upper()  # nosec
    -
    -    logger.info("Magnet Info Hash: %s", infohash)
    -    scheme = "magnet:"
    -    hasharg = "?xt=urn:btih:" + infohash
    -    namearg = "&dn=" + quote_plus(info["name"])
    -
    -    if "announce-list" in meta:
    -        announce_args = [
    -            "&tr=" + quote_plus(url)
    -            for urllist in meta["announce-list"]
    -            for url in urllist
    -        ]
    -    else:
    -        announce_args = ["&tr=" + quote_plus(meta["announce"])]
    -
    -    full_uri = "".join([scheme, hasharg, namearg] + announce_args)
    -    logger.info("Created Magnet URI %s", full_uri)
    -    sys.stdout.write("\n" + full_uri + "\n")
    -    return full_uri
    -
    -
    -

    @@ -2335,12 +1389,12 @@

    -

    +

    __main__ -

    +
    @@ -2357,40 +1411,6 @@

    -
    - - - -

    -main() - - -

    - - -
    - -

    Start the entry point script.

    - -
    - Source code in torrentfile\__main__.py -
    26
    -27
    -28
    -29
    -30
    def main():
    -    """
    -    Start the entry point script.
    -    """
    -    execute()
    -
    -
    -
    -
    - -
    - - @@ -2406,12 +1426,12 @@

    -

    +

    cli -

    +
    @@ -2438,12 +1458,12 @@

    -

    +

    TorrentFileHelpFormatter -

    +
    @@ -2455,202 +1475,6 @@

    Subclasses Argparse.HelpFormatter.

    -
    - Source code in torrentfile\cli.py -
     70
    - 71
    - 72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    class TorrentFileHelpFormatter(HelpFormatter):
    -    """
    -    Formatting class for help tips provided by the CLI.
    -
    -    Subclasses Argparse.HelpFormatter.
    -    """
    -
    -    def __init__(self, prog, width=40, max_help_positions=30):
    -        """
    -        Construct HelpFormat class for usage output.
    -
    -        Parameters
    -        ----------
    -        prog : str
    -            Name of the program.
    -        width : int
    -            Max width of help message output.
    -        max_help_positions : int
    -            max length until line wrap.
    -        """
    -        super().__init__(
    -            prog, width=width, max_help_position=max_help_positions
    -        )
    -
    -    def _split_lines(self, text, _):
    -        """
    -        Split multiline help messages and remove indentation.
    -
    -        Parameters
    -        ----------
    -        text : str
    -            text that needs to be split
    -        _ : int
    -            max width for line.
    -        """
    -        lines = text.split("\n")
    -        return [line.strip() for line in lines if line]
    -
    -    def _format_text(self, text):
    -        """
    -        Format text for cli usage messages.
    -
    -        Parameters
    -        ----------
    -        text : str
    -            Pre-formatted text.
    -
    -        Returns
    -        -------
    -        str
    -            Formatted text from input.
    -        """
    -        text = text % dict(prog=self._prog) if "%(prog)" in text else text
    -        text = self._whitespace_matcher.sub(" ", text).strip()
    -        return text + "\n\n"
    -
    -    def _join_parts(self, part_strings):
    -        """
    -        Combine different sections of the help message.
    -
    -        Parameters
    -        ----------
    -        part_strings : list
    -            List of argument help messages and headers.
    -
    -        Returns
    -        -------
    -        str
    -            Fully formatted help message for CLI.
    -        """
    -        parts = self.format_headers(part_strings)
    -        return super()._join_parts(parts)
    -
    -    @staticmethod
    -    def format_headers(parts):
    -        """
    -        Format help message section headers.
    -
    -        Parameters
    -        ----------
    -        parts : list
    -            List of individual lines for help message.
    -
    -        Returns
    -        -------
    -        list
    -            Input list with formatted section headers.
    -        """
    -        if parts and parts[0].startswith("usage:"):
    -            parts[0] = "Usage\n=====\n  " + parts[0][6:]
    -        headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
    -        for i in headings[::-1]:
    -            parts[i] = parts[i][:-2].title()
    -            underline = "".join(["\n", "-" * len(parts[i]), "\n"])
    -            parts.insert(i + 1, underline)
    -        return parts
    -
    -
    -
    @@ -2667,11 +1491,11 @@

    -

    +

    __init__(prog, width=40, max_help_positions=30) -

    +
    @@ -2722,42 +1546,6 @@
    - Source code in torrentfile\cli.py -
    77
    -78
    -79
    -80
    -81
    -82
    -83
    -84
    -85
    -86
    -87
    -88
    -89
    -90
    -91
    -92
    def __init__(self, prog, width=40, max_help_positions=30):
    -    """
    -    Construct HelpFormat class for usage output.
    -
    -    Parameters
    -    ----------
    -    prog : str
    -        Name of the program.
    -    width : int
    -        Max width of help message output.
    -    max_help_positions : int
    -        max length until line wrap.
    -    """
    -    super().__init__(
    -        prog, width=width, max_help_position=max_help_positions
    -    )
    -
    -
    -
    @@ -2768,11 +1556,11 @@
    +

    _format_text(text) -

    +
    @@ -2821,44 +1609,6 @@
    - Source code in torrentfile\cli.py -
    108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    def _format_text(self, text):
    -    """
    -    Format text for cli usage messages.
    -
    -    Parameters
    -    ----------
    -    text : str
    -        Pre-formatted text.
    -
    -    Returns
    -    -------
    -    str
    -        Formatted text from input.
    -    """
    -    text = text % dict(prog=self._prog) if "%(prog)" in text else text
    -    text = self._whitespace_matcher.sub(" ", text).strip()
    -    return text + "\n\n"
    -
    -
    -
    @@ -2869,11 +1619,11 @@
    +

    _join_parts(part_strings) -

    +
    @@ -2922,42 +1672,6 @@
    - Source code in torrentfile\cli.py -
    126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    def _join_parts(self, part_strings):
    -    """
    -    Combine different sections of the help message.
    -
    -    Parameters
    -    ----------
    -    part_strings : list
    -        List of argument help messages and headers.
    -
    -    Returns
    -    -------
    -    str
    -        Fully formatted help message for CLI.
    -    """
    -    parts = self.format_headers(part_strings)
    -    return super()._join_parts(parts)
    -
    -
    -
    @@ -2968,11 +1682,11 @@
    +

    _split_lines(text, _) -

    +
    @@ -3013,36 +1727,6 @@
    - Source code in torrentfile\cli.py -
     94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    def _split_lines(self, text, _):
    -    """
    -    Split multiline help messages and remove indentation.
    -
    -    Parameters
    -    ----------
    -    text : str
    -        text that needs to be split
    -    _ : int
    -        max width for line.
    -    """
    -    lines = text.split("\n")
    -    return [line.strip() for line in lines if line]
    -
    -
    -
    @@ -3053,14 +1737,14 @@
    +

    format_headers(parts) staticmethod -

    +
    @@ -3109,56 +1793,6 @@
    - Source code in torrentfile\cli.py -
    143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    @staticmethod
    -def format_headers(parts):
    -    """
    -    Format help message section headers.
    -
    -    Parameters
    -    ----------
    -    parts : list
    -        List of individual lines for help message.
    -
    -    Returns
    -    -------
    -    list
    -        Input list with formatted section headers.
    -    """
    -    if parts and parts[0].startswith("usage:"):
    -        parts[0] = "Usage\n=====\n  " + parts[0][6:]
    -    headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
    -    for i in headings[::-1]:
    -        parts[i] = parts[i][:-2].title()
    -        underline = "".join(["\n", "-" * len(parts[i]), "\n"])
    -        parts.insert(i + 1, underline)
    -    return parts
    -
    -
    -
    @@ -3180,77 +1814,17 @@
    +

    activate_logger() -

    +

    Activate the builtin logging mechanism when passed debug flag from CLI.

    -
    - Source code in torrentfile\cli.py -
    40
    -41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    def activate_logger():
    -    """
    -    Activate the builtin logging mechanism when passed debug flag from CLI.
    -    """
    -    logging.basicConfig(level=logging.INFO)
    -    logger = logging.getLogger()
    -    # file_handler = logging.FileHandler(
    -    #     "torrentfile.log", mode="a+", encoding="utf-8"
    -    # )
    -    console_handler = logging.StreamHandler(stream=sys.stderr)
    -    # file_formatter = logging.Formatter(
    -    #     "%(asctime)s %(levelno)s %(message)s",
    -    #     datefmt="%m-%d %H:%M:%S",
    -    #     style="%",
    -    # )
    -    stream_formatter = logging.Formatter(
    -        "%(asctime)s %(levelno)s %(message)s",
    -        datefmt="%m-%d %H:%M:%S",
    -        style="%",
    -    )
    -    # file_handler.setFormatter(file_formatter)
    -    console_handler.setFormatter(stream_formatter)
    -    # file_handler.setLevel(logging.INFO)
    -    console_handler.setLevel(logging.DEBUG)
    -    logger.setLevel(logging.DEBUG)
    -    logger.addHandler(console_handler)
    -    # logger.addHandler(file_handler)
    -    logger.debug("Debug: ON")
    -
    -
    -
    @@ -3261,11 +1835,11 @@

    -

    +

    execute(args=None) -

    +
    @@ -3314,716 +1888,6 @@

    -
    - Source code in torrentfile\cli.py -
    168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    -403
    -404
    -405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    -519
    -520
    def execute(args=None) -> list:
    -    """
    -    Initialize Command Line Interface for torrentfile.
    -
    -    Parameters
    -    ----------
    -    args : list
    -        Commandline arguments. default=None
    -
    -    Returns
    -    -------
    -    list
    -        Depends on what the command line args were.
    -    """
    -    if not args:
    -        if sys.argv[1:]:
    -            args = sys.argv[1:]
    -        else:
    -            args = ["-h"]
    -
    -    parser = ArgumentParser(
    -        "torrentfile",
    -        description=(
    -            "Command line tools for creating, editing, checking and "
    -            "interacting with Bittorrent metainfo files"
    -        ),
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -        conflict_handler="resolve",
    -    )
    -
    -    parser.add_argument(
    -        "-i",
    -        "--interactive",
    -        action="store_true",
    -        dest="interactive",
    -        help="select program options interactively",
    -    )
    -
    -    parser.add_argument(
    -        "-V",
    -        "--version",
    -        action="version",
    -        version=f"torrentfile v{version}",
    -        help="show program version and exit",
    -    )
    -
    -    parser.add_argument(
    -        "-v",
    -        "--verbose",
    -        action="store_true",
    -        dest="debug",
    -        help="output debug information",
    -    )
    -
    -    parser.set_defaults(func=parser.print_help)
    -
    -    subparsers = parser.add_subparsers(
    -        title="Actions",
    -        dest="command",
    -        metavar="create, edit, magnet, recheck",
    -    )
    -
    -    create_parser = subparsers.add_parser(
    -        "create",
    -        help="""Generate a new torrent meta file.""",
    -        prefix_chars="-",
    -        aliases=["c", "new"],
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    create_parser.add_argument(
    -        "-a",
    -        "-t",
    -        "--announce",
    -        "--tracker",
    -        action="store",
    -        dest="announce",
    -        metavar="<url>",
    -        nargs="+",
    -        default=[],
    -        help="One or more space-seperated torrent tracker url(s).",
    -    )
    -
    -    create_parser.add_argument(
    -        "-p",
    -        "--private",
    -        action="store_true",
    -        dest="private",
    -        help="Creates private torrent with multi-tracker and DHT turned off.",
    -    )
    -
    -    create_parser.add_argument(
    -        "-s",
    -        "--source",
    -        action="store",
    -        dest="source",
    -        metavar="<source>",
    -        help="Add a source string. Useful for cross-seeding.",
    -    )
    -
    -    create_parser.add_argument(
    -        "-m",
    -        "--magnet",
    -        action="store_true",
    -        dest="magnet",
    -    )
    -
    -    create_parser.add_argument(
    -        "-c",
    -        "--comment",
    -        action="store",
    -        dest="comment",
    -        metavar="<comment>",
    -        help="Include a comment in file metadata",
    -    )
    -
    -    create_parser.add_argument(
    -        "-o",
    -        "--out",
    -        action="store",
    -        dest="outfile",
    -        metavar="<path>",
    -        help="Output save path for created .torrent file",
    -    )
    -
    -    create_parser.add_argument(
    -        "--cwd",
    -        "--current",
    -        action="store_true",
    -        dest="cwd",
    -        help="Save output .torrent file to current directory",
    -    )
    -
    -    create_parser.add_argument(
    -        "--prog",
    -        "--progress",
    -        default="1",
    -        action="store",
    -        dest="progress",
    -        help="""
    -        Set the progress bar level.
    -        Options = 0, 1
    -        (0) = Do not display progress bar.
    -        (1) = Display progress bar.
    -        """,
    -    )
    -
    -    create_parser.add_argument(
    -        "--meta-version",
    -        default="1",
    -        choices=["1", "2", "3"],
    -        action="store",
    -        dest="meta_version",
    -        metavar="<int>",
    -        help="""
    -        Bittorrent metafile version.
    -        Options = 1, 2, 3
    -        (1) = Bittorrent v1 (Default)
    -        (2) = Bittorrent v2
    -        (3) = Bittorrent v1 & v2 hybrid
    -        """,
    -    )
    -
    -    create_parser.add_argument(
    -        "--piece-length",
    -        action="store",
    -        dest="piece_length",
    -        metavar="<int>",
    -        help="""
    -        (Default: <blank>) Number of bytes for per chunk of data transmitted
    -        by Bittorrent client. Acceptable values include integers 14-26 which
    -        will be interpreted as a perfect power of 2.  e.g. 14 = 16KiB pieces.
    -        Examples:: [--piece-length 14] [--piece-length 20]
    -        """,
    -    )
    -
    -    create_parser.add_argument(
    -        "-w",
    -        "--web-seed",
    -        action="store",
    -        dest="url_list",
    -        metavar="<url>",
    -        nargs="+",
    -        help="list of web addresses where torrent data exists (GetRight).",
    -    )
    -
    -    create_parser.add_argument(
    -        "--http-seed",
    -        action="store",
    -        dest="httpseeds",
    -        metavar="<url>",
    -        nargs="+",
    -        help="list of URLs, addresses where content can be found (Hoffman).",
    -    )
    -
    -    create_parser.add_argument(
    -        "content",
    -        action="store",
    -        metavar="<content>",
    -        nargs="?",
    -        help="Path to content file or directory",
    -    )
    -
    -    create_parser.set_defaults(func=create)
    -
    -    edit_parser = subparsers.add_parser(
    -        "edit",
    -        help="""
    -        Edit existing torrent meta file.
    -        """,
    -        aliases=["e"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    edit_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        help="path to *.torrent file",
    -        metavar="<*.torrent>",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--tracker",
    -        action="store",
    -        dest="announce",
    -        metavar="<url>",
    -        nargs="+",
    -        help="""
    -        Replace current list of tracker/announce urls with one or more space
    -        seperated Bittorrent tracker announce url(s).
    -        """,
    -    )
    -
    -    edit_parser.add_argument(
    -        "--web-seed",
    -        action="store",
    -        dest="url_list",
    -        metavar="<url>",
    -        nargs="+",
    -        help="Replace current list of web-seed urls with one or more url(s)",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--http-seed",
    -        action="store",
    -        dest="httpseeds",
    -        metavar="<url>",
    -        nargs="+",
    -        help="replace all currently listed addresses with new list (Hoffman).",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--private",
    -        action="store_true",
    -        help="Make torrent private.",
    -        dest="private",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--comment",
    -        help="Replaces any existing comment with <comment>",
    -        metavar="<comment>",
    -        dest="comment",
    -        action="store",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--source",
    -        action="store",
    -        dest="source",
    -        metavar="<source>",
    -        help="Replaces current source with <source>",
    -    )
    -
    -    edit_parser.set_defaults(func=edit)
    -
    -    magnet_parser = subparsers.add_parser(
    -        "magnet",
    -        help="""
    -        Generate magnet url from an existing Bittorrent meta file.
    -        """,
    -        aliases=["m"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    magnet_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        help="Path to Bittorrent meta file.",
    -        metavar="<*.torrent>",
    -    )
    -
    -    magnet_parser.set_defaults(func=magnet)
    -
    -    check_parser = subparsers.add_parser(
    -        "recheck",
    -        help="""
    -        Calculate amount of torrent meta file's content is found on disk.
    -        """,
    -        aliases=["r", "check"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    check_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        metavar="<*.torrent>",
    -        help="path to .torrent file.",
    -    )
    -
    -    check_parser.add_argument(
    -        "content",
    -        action="store",
    -        metavar="<content>",
    -        help="path to content file or directory",
    -    )
    -
    -    check_parser.set_defaults(func=recheck)
    -
    -    info_parser = subparsers.add_parser(
    -        "info",
    -        help="""
    -        Show detailed information about a torrent file.
    -        """,
    -        aliases=["i"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    info_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        metavar="<*.torrent>",
    -        help="path to pre-existing torrent file.",
    -    )
    -
    -    info_parser.set_defaults(func=info)
    -
    -    args = parser.parse_args(args)
    -
    -    if args.debug:
    -        activate_logger()
    -
    -    if args.interactive:
    -        return select_action()
    -
    -    if hasattr(args, "func"):
    -        return args.func(args)
    -    return args
    -
    -
    -
    @@ -4034,31 +1898,17 @@

    -

    +

    main() -

    +

    Initiate main function for CLI script.

    -
    - Source code in torrentfile\cli.py -
    526
    -527
    -528
    -529
    -530
    def main():
    -    """
    -    Initiate main function for CLI script.
    -    """
    -    execute()
    -
    -
    -
    @@ -4079,19 +1929,19 @@

    -

    +

    commands -

    +

    The commands module contains the Action Commands executed by the CLI script.

    Each function pertains to a command line action/subcommand and drives specific features of the application.

    -
    Functions
    +

    Functions

    create_command info_command edit_command @@ -4115,11 +1965,11 @@

    Functions
    -

    +

    create(args) -

    +
    @@ -4168,76 +2018,6 @@

    -
    - Source code in torrentfile\commands.py -
    49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    -77
    -78
    -79
    -80
    -81
    def create(args: list):
    -    """
    -    Execute the create CLI sub-command to create a new torrent metafile.
    -
    -    Parameters
    -    ----------
    -    args : argparse.Namespace
    -        positional and optional CLI arguments.
    -
    -    Returns
    -    -------
    -    torrentfile.MetaFile
    -        object containing the path to created metafile and its contents.
    -    """
    -    kwargs = vars(args)
    -    logger.debug("Creating torrent from %s", args.content)
    -    if args.meta_version == "1":
    -        torrent = TorrentFile(**kwargs)
    -    else:
    -        torrent = TorrentAssembler(**kwargs)
    -    outfile, meta = torrent.write()
    -
    -    if args.magnet:
    -        magnet(outfile)
    -
    -    args.torrent = torrent
    -    args.kwargs = kwargs
    -    args.outfile = outfile
    -    args.meta = meta
    -
    -    print("\nTorrent Save Path: ", os.path.abspath(str(outfile)))
    -    logger.debug("Output path: %s", str(outfile))
    -    return args
    -
    -
    -

    @@ -4248,11 +2028,11 @@

    -

    +

    edit(args) -

    +
    @@ -4301,60 +2081,6 @@

    -
    - Source code in torrentfile\commands.py -
    121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    -144
    -145
    def edit(args: list):
    -    """
    -    Execute the edit CLI sub-command with provided arguments.
    -
    -    Parameters
    -    ----------
    -    args : Namespace
    -        positional and optional CLI arguments.
    -
    -    Returns
    -    -------
    -    str
    -        path to edited torrent file.
    -    """
    -    metafile = args.metafile
    -    logger.info("Editing %s Meta File", str(args.metafile))
    -    editargs = {
    -        "url-list": args.url_list,
    -        "httpseeds": args.httpseeds,
    -        "announce": args.announce,
    -        "source": args.source,
    -        "private": args.private,
    -        "comment": args.comment,
    -    }
    -    return edit_torrent(metafile, editargs)
    -
    -
    -
    @@ -4365,11 +2091,11 @@

    -

    +

    info(args) -

    +
    @@ -4400,80 +2126,6 @@

    -
    - Source code in torrentfile\commands.py -
     84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    def info(args: list):
    -    """
    -    Show torrent metafile details to user via stdout.
    -
    -    Parameters
    -    ----------
    -    args : dict
    -        command line arguements provided by the user.
    -    """
    -    metafile = args.metafile
    -    meta = pyben.load(metafile)
    -    info = meta["info"]
    -    del meta["info"]
    -    meta.update(info)
    -    if "private" in meta and meta["private"] == 1:
    -        meta["private"] = "True"
    -    if "announce-list" in meta:
    -        lst = meta["announce-list"]
    -        meta["announce-list"] = ", ".join([j for i in lst for j in i])
    -    if "url-list" in meta:
    -        meta["url-list"] = ", ".join(meta["url-list"])
    -    if "httpseeds" in meta:
    -        meta["httpseeds"] = ", ".join(meta["httpseeds"])
    -    text = []
    -    longest = max([len(i) for i in meta.keys()])
    -    for key, val in meta.items():
    -        if key not in ["pieces", "piece layers", "files", "file tree"]:
    -            prefix = longest - len(key) + 1
    -            string = key + (" " * prefix) + str(val)
    -            text.append(string)
    -    most = max([len(i) for i in text])
    -    text = ["-" * most, "\n"] + text + ["\n", "-" * most]
    -    output = "\n".join(text)
    -    print(output)
    -    return output
    -
    -
    -

    @@ -4484,11 +2136,11 @@

    -

    +

    magnet(metafile) -

    +
    @@ -4537,94 +2189,6 @@

    -
    - Source code in torrentfile\commands.py -
    175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    def magnet(metafile: str):
    -    """
    -    Create a magnet URI from a Bittorrent meta file.
    -
    -    Parameters
    -    ----------
    -    metafile : (Namespace||str)
    -        Namespace class for CLI arguments.
    -
    -    Returns
    -    -------
    -    str
    -        created magnet URI.
    -    """
    -    if hasattr(metafile, "metafile"):
    -        metafile = metafile.metafile
    -    if not os.path.exists(metafile):
    -        raise FileNotFoundError
    -
    -    meta = pyben.load(metafile)
    -    info = meta["info"]
    -    binfo = pyben.dumps(info)
    -    infohash = sha1(binfo).hexdigest().upper()  # nosec
    -
    -    logger.info("Magnet Info Hash: %s", infohash)
    -    scheme = "magnet:"
    -    hasharg = "?xt=urn:btih:" + infohash
    -    namearg = "&dn=" + quote_plus(info["name"])
    -
    -    if "announce-list" in meta:
    -        announce_args = [
    -            "&tr=" + quote_plus(url)
    -            for urllist in meta["announce-list"]
    -            for url in urllist
    -        ]
    -    else:
    -        announce_args = ["&tr=" + quote_plus(meta["announce"])]
    -
    -    full_uri = "".join([scheme, hasharg, namearg] + announce_args)
    -    logger.info("Created Magnet URI %s", full_uri)
    -    sys.stdout.write("\n" + full_uri + "\n")
    -    return full_uri
    -
    -
    -
    @@ -4635,11 +2199,11 @@

    -

    +

    recheck(args) -

    +
    @@ -4688,60 +2252,6 @@

    -
    - Source code in torrentfile\commands.py -
    148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    def recheck(args: list):
    -    """
    -    Execute recheck CLI sub-command.
    -
    -    Parameters
    -    ----------
    -    args : Namespace
    -        positional and optional arguments.
    -
    -    Returns
    -    -------
    -    str
    -        The percentage of content currently saved to disk.
    -    """
    -    metafile = args.metafile
    -    content = args.content
    -    logger.debug("Validating %s against %s contents", metafile, content)
    -    checker = Checker(metafile, content)
    -
    -    logger.debug("Completed initialization of the Checker class")
    -    result = checker.results()
    -
    -    sys.stdout.write(str(result) + "% Match\n")
    -    sys.stdout.flush()
    -    return result
    -
    -
    -

    @@ -4762,12 +2272,12 @@

    -

    +

    edit -

    +
    @@ -4778,7 +2288,7 @@

    are chosen to edit, this command can trigger a new info hash which means the torrent will no longer be able to participate in the same swarm as the original unedited torrent.

    -

    Keywords
    +

    Keywords

    private comment source @@ -4801,11 +2311,11 @@

    Keywords
    -

    +

    edit_torrent(metafile, args) -

    +
    @@ -4864,126 +2374,6 @@

    -
    - Source code in torrentfile\edit.py -
     73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    def edit_torrent(metafile: str, args: dict) -> dict:
    -    """
    -    Edit the properties and values in a torrent meta file.
    -
    -    Parameters
    -    ----------
    -    metafile : str
    -        path to the torrent meta file.
    -    args : dict
    -        key value pairs of the properties to be edited.
    -
    -    Returns
    -    -------
    -    dict
    -        The edited and nested Meta and info dictionaries.
    -    """
    -    logger.debug("editing torrent file %s", metafile)
    -    meta = pyben.load(metafile)
    -    info = meta["info"]
    -    filter_empty(args, meta, info)
    -
    -    if "comment" in args:
    -        info["comment"] = args["comment"]
    -
    -    if "source" in args:
    -        info["source"] = args["source"]
    -
    -    if "private" in args:
    -        info["private"] = 1
    -
    -    if "announce" in args:
    -        val = args.get("announce", None)
    -        if isinstance(val, str):
    -            vallist = val.split()
    -            meta["announce"] = vallist[0]
    -            meta["announce-list"] = [vallist]
    -        elif isinstance(val, list):
    -            meta["announce"] = val[0]
    -            meta["announce-list"] = [val]
    -
    -    if "url-list" in args:
    -        val = args.get("url-list")
    -        if isinstance(val, str):
    -            meta["url-list"] = val.split()
    -        elif isinstance(val, list):
    -            meta["url-list"] = val
    -
    -    if "httpseeds" in args:
    -        val = args.get("httpseeds")
    -        if isinstance(val, str):
    -            meta["httpseeds"] = val.split()
    -        elif isinstance(val, list):
    -            meta["httpseeds"] = val
    -
    -    meta["info"] = info
    -    os.remove(metafile)
    -    pyben.dump(meta, metafile)
    -    return meta
    -
    -
    -
    @@ -4994,11 +2384,11 @@

    -

    +

    filter_empty(args, meta, info) -

    +
    @@ -5049,60 +2439,6 @@

    -
    - Source code in torrentfile\edit.py -
    46
    -47
    -48
    -49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    def filter_empty(args: dict, meta: dict, info: dict):
    -    """
    -    Remove dictionary keys with empty values.
    -
    -    Parameters
    -    ----------
    -    args : dict
    -        Editable metafile properties from user.
    -    meta : dict
    -        Metafile data dictionary.
    -    info : dict
    -        Metafile info dictionary.
    -    """
    -    for key, val in list(args.items()):
    -        if val is None:
    -            del args[key]
    -            continue
    -
    -        if val == "":
    -            if key in meta:
    -                del meta[key]
    -            elif key in info:
    -                del info[key]
    -            del args[key]
    -            logger.debug("removeing empty fields %s", val)
    -
    -
    -

    @@ -5123,12 +2459,12 @@

    -

    +

    hasher -

    +
    @@ -5151,12 +2487,12 @@

    -

    +

    FileHasher -

    +
    @@ -5214,280 +2550,6 @@

    -
    - Source code in torrentfile\hasher.py -
    400
    -401
    -402
    -403
    -404
    -405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    class FileHasher(CbMixin, ProgMixin):
    -    """
    -    Calculate root and piece hashes for creating hybrid torrent file.
    -
    -    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
    -    With a branching factor of 2, merge layer hashes until blocks equal
    -    piece_length bytes for the piece layer, and then the root hash.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        path to target file.
    -    piece_length : int
    -        piece length for data chunks.
    -    progress : int
    -        default = None
    -    """
    -
    -    def __init__(
    -        self,
    -        path: str,
    -        piece_length: int,
    -        progress: bool = True,
    -        hybrid: bool = False,
    -    ):
    -        """
    -        Construct Hasher class instances for each file in torrent.
    -        """
    -        self.path = path
    -        self.piece_length = piece_length
    -        self.pieces = []
    -        self.layer_hashes = []
    -        self.piece_layer = None
    -        self.root = None
    -        self.padding_piece = None
    -        self.padding_file = None
    -        self.amount = piece_length // BLOCK_SIZE
    -        self.end = False
    -        self.current = open(path, "rb")
    -        self.hybrid = hybrid
    -        if progress:
    -            self.progressbar = True
    -            self.prog_start(os.path.getsize(path), path, unit="bytes")
    -
    -    def __iter__(self):
    -        """Return `self`: needed to implement iterator implementation."""
    -        return self
    -
    -    def _pad_remaining(self, block_count: int):
    -        """
    -        Generate Hash sized, 0 filled bytes for padding.
    -
    -        Parameters
    -        ----------
    -        block_count : int
    -            current total number of blocks collected.
    -
    -        Returns
    -        -------
    -        padding : bytes
    -            Padding to fill remaining portion of tree.
    -        """
    -        # when the there is only one block for file
    -        remaining = self.amount - block_count
    -        if not self.layer_hashes:
    -            power2 = next_power_2(block_count)
    -            remaining = power2 - block_count
    -        return [bytes(HASH_SIZE) for _ in range(remaining)]
    -
    -    def __next__(self) -> bytes:
    -        """
    -        Calculate layer hashes for contents of file.
    -
    -        Returns
    -        -------
    -        bytes
    -            The layer merckle root hash.
    -        """
    -        if self.end:
    -            self.end = False
    -            raise StopIteration
    -        plength = self.piece_length
    -        blocks = []
    -        piece = sha1()  # nosec
    -        total = 0
    -        block = bytearray(BLOCK_SIZE)
    -        for _ in range(self.amount):
    -            size = self.current.readinto(block)
    -            if not size:
    -                self.end = True
    -                break
    -            total += size
    -            plength -= size
    -            blocks.append(sha256(block[:size]).digest())
    -            if self.hybrid:
    -                piece.update(block[:size])
    -        if len(blocks) != self.amount:
    -            padding = self._pad_remaining(len(blocks))
    -            blocks.extend(padding)
    -        self.prog_update(total)
    -        layer_hash = merkle_root(blocks)
    -        self.layer_hashes.append(layer_hash)
    -        if self._cb:
    -            self._cb(layer_hash)
    -        if self.end:
    -            self._calculate_root()
    -            self.prog_close()
    -        if self.hybrid:
    -            if plength > 0:
    -                self.padding_file = {
    -                    "attr": "p",
    -                    "length": plength,
    -                    "path": [".pad", str(plength)],
    -                }
    -                piece.update(bytes(plength))
    -            piece = piece.digest()
    -            self.pieces.append(piece)
    -            return layer_hash, piece
    -        return layer_hash
    -
    -    def _calculate_root(self):
    -        """
    -        Calculate the root hash for opened file.
    -        """
    -        self.piece_layer = b"".join(self.layer_hashes)
    -
    -        if len(self.layer_hashes) > 1:
    -            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
    -
    -            pow2 = next_power_2(len(self.layer_hashes))
    -            remainder = pow2 - len(self.layer_hashes)
    -
    -            self.layer_hashes += [pad_piece for _ in range(remainder)]
    -        self.root = merkle_root(self.layer_hashes)
    -        self.current.close()
    -
    -
    -
    @@ -5517,71 +2579,17 @@

    -

    +

    __init__(path, piece_length, progress=True, hybrid=False) -

    +

    Construct Hasher class instances for each file in torrent.

    -
    - Source code in torrentfile\hasher.py -
    418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    def __init__(
    -    self,
    -    path: str,
    -    piece_length: int,
    -    progress: bool = True,
    -    hybrid: bool = False,
    -):
    -    """
    -    Construct Hasher class instances for each file in torrent.
    -    """
    -    self.path = path
    -    self.piece_length = piece_length
    -    self.pieces = []
    -    self.layer_hashes = []
    -    self.piece_layer = None
    -    self.root = None
    -    self.padding_piece = None
    -    self.padding_file = None
    -    self.amount = piece_length // BLOCK_SIZE
    -    self.end = False
    -    self.current = open(path, "rb")
    -    self.hybrid = hybrid
    -    if progress:
    -        self.progressbar = True
    -        self.prog_start(os.path.getsize(path), path, unit="bytes")
    -
    -
    -
    @@ -5592,27 +2600,17 @@
    -
    +

    __iter__() -

    +

    Return self: needed to implement iterator implementation.

    -
    - Source code in torrentfile\hasher.py -
    444
    -445
    -446
    def __iter__(self):
    -    """Return `self`: needed to implement iterator implementation."""
    -    return self
    -
    -
    -
    @@ -5623,11 +2621,11 @@
    -
    +

    __next__() -

    +
    @@ -5652,110 +2650,6 @@
    -
    - Source code in torrentfile\hasher.py -
    469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    def __next__(self) -> bytes:
    -    """
    -    Calculate layer hashes for contents of file.
    -
    -    Returns
    -    -------
    -    bytes
    -        The layer merckle root hash.
    -    """
    -    if self.end:
    -        self.end = False
    -        raise StopIteration
    -    plength = self.piece_length
    -    blocks = []
    -    piece = sha1()  # nosec
    -    total = 0
    -    block = bytearray(BLOCK_SIZE)
    -    for _ in range(self.amount):
    -        size = self.current.readinto(block)
    -        if not size:
    -            self.end = True
    -            break
    -        total += size
    -        plength -= size
    -        blocks.append(sha256(block[:size]).digest())
    -        if self.hybrid:
    -            piece.update(block[:size])
    -    if len(blocks) != self.amount:
    -        padding = self._pad_remaining(len(blocks))
    -        blocks.extend(padding)
    -    self.prog_update(total)
    -    layer_hash = merkle_root(blocks)
    -    self.layer_hashes.append(layer_hash)
    -    if self._cb:
    -        self._cb(layer_hash)
    -    if self.end:
    -        self._calculate_root()
    -        self.prog_close()
    -    if self.hybrid:
    -        if plength > 0:
    -            self.padding_file = {
    -                "attr": "p",
    -                "length": plength,
    -                "path": [".pad", str(plength)],
    -            }
    -            piece.update(bytes(plength))
    -        piece = piece.digest()
    -        self.pieces.append(piece)
    -        return layer_hash, piece
    -    return layer_hash
    -
    -
    -
    @@ -5766,51 +2660,17 @@
    -
    +

    _calculate_root() -

    +

    Calculate the root hash for opened file.

    -
    - Source code in torrentfile\hasher.py -
    520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    def _calculate_root(self):
    -    """
    -    Calculate the root hash for opened file.
    -    """
    -    self.piece_layer = b"".join(self.layer_hashes)
    -
    -    if len(self.layer_hashes) > 1:
    -        pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
    -
    -        pow2 = next_power_2(len(self.layer_hashes))
    -        remainder = pow2 - len(self.layer_hashes)
    -
    -        self.layer_hashes += [pad_piece for _ in range(remainder)]
    -    self.root = merkle_root(self.layer_hashes)
    -    self.current.close()
    -
    -
    -
    @@ -5821,11 +2681,11 @@
    -
    +

    _pad_remaining(block_count) -

    +
    @@ -5874,50 +2734,6 @@
    -
    - Source code in torrentfile\hasher.py -
    448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    def _pad_remaining(self, block_count: int):
    -    """
    -    Generate Hash sized, 0 filled bytes for padding.
    -
    -    Parameters
    -    ----------
    -    block_count : int
    -        current total number of blocks collected.
    -
    -    Returns
    -    -------
    -    padding : bytes
    -        Padding to fill remaining portion of tree.
    -    """
    -    # when the there is only one block for file
    -    remaining = self.amount - block_count
    -    if not self.layer_hashes:
    -        power2 = next_power_2(block_count)
    -        remaining = power2 - block_count
    -    return [bytes(HASH_SIZE) for _ in range(remaining)]
    -
    -
    -
    @@ -5938,12 +2754,12 @@
    -

    +

    Hasher -

    +
    @@ -6001,226 +2817,6 @@

    -
    - Source code in torrentfile\hasher.py -
     36
    - 37
    - 38
    - 39
    - 40
    - 41
    - 42
    - 43
    - 44
    - 45
    - 46
    - 47
    - 48
    - 49
    - 50
    - 51
    - 52
    - 53
    - 54
    - 55
    - 56
    - 57
    - 58
    - 59
    - 60
    - 61
    - 62
    - 63
    - 64
    - 65
    - 66
    - 67
    - 68
    - 69
    - 70
    - 71
    - 72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    class Hasher(CbMixin, ProgMixin):
    -    """
    -    Piece hasher for Bittorrent V1 files.
    -
    -    Takes a sorted list of all file paths, calculates sha1 hash
    -    for fixed size pieces of file data from each file
    -    seemlessly until the last piece which may be smaller than others.
    -
    -    Parameters
    -    ----------
    -    paths : list
    -        List of files.
    -    piece_length : int
    -        Size of chuncks to split the data into.
    -    progress : int
    -        default = None
    -    """
    -
    -    def __init__(self, paths: list, piece_length: int, progress: bool = True):
    -        """Generate hashes of piece length data from filelist contents."""
    -        self.piece_length = piece_length
    -        self.paths = paths
    -        self.progress = progress
    -        self.total = sum([os.path.getsize(i) for i in self.paths])
    -        self.index = 0
    -        self.current = open(self.paths[0], "rb")
    -        if self.progress:
    -            total = os.path.getsize(self.paths[0])
    -            self.prog_start(total, self.paths[0], unit="bytes")
    -        logger.debug("Hashing %s", str(self.paths[0]))
    -
    -    def __iter__(self):
    -        """
    -        Iterate through feed pieces.
    -
    -        Returns
    -        -------
    -        self : iterator
    -            Iterator for leaves/hash pieces.
    -        """
    -        return self
    -
    -    def _handle_partial(self, arr: bytearray) -> bytearray:
    -        """
    -        Define the handling partial pieces that span 2 or more files.
    -
    -        Parameters
    -        ----------
    -        arr : bytearray
    -            Incomplete piece containing partial data
    -
    -        Returns
    -        -------
    -        digest : bytearray
    -            SHA1 digest of the complete piece.
    -        """
    -        while len(arr) < self.piece_length and self.next_file():
    -            target = self.piece_length - len(arr)
    -            temp = bytearray(target)
    -            size = self.current.readinto(temp)
    -            arr.extend(temp[:size])
    -            self.prog_update(size)
    -            if size == target:
    -                break
    -        return sha1(arr).digest()  # nosec
    -
    -    def next_file(self) -> bool:
    -        """
    -        Seemlessly transition to next file in file list.
    -
    -        Returns
    -        -------
    -        bool:
    -            True if there is a next file otherwise False.
    -        """
    -        self.index += 1
    -        self.prog_close()
    -        if self.index < len(self.paths):
    -            path = self.paths[self.index]
    -            logger.debug("Hashing %s", str(path))
    -            self.current.close()
    -            if self.progress:
    -                self.prog_start(os.path.getsize(path), path, unit="bytes")
    -            self.current = open(path, "rb")
    -            return True
    -        return False
    -
    -    def __next__(self) -> bytes:
    -        """
    -        Generate piece-length pieces of data from input file list.
    -
    -        Returns
    -        -------
    -        bytes
    -            SHA1 hash of the piece extracted.
    -        """
    -        while True:
    -            piece = bytearray(self.piece_length)
    -            size = self.current.readinto(piece)
    -            if size == 0:
    -                if not self.next_file():
    -                    raise StopIteration
    -            elif size < self.piece_length:
    -                self.prog_update(size)
    -                return self._handle_partial(piece[:size])
    -            else:
    -                self.prog_update(size)
    -                return sha1(piece).digest()  # nosec
    -
    -
    -
    @@ -6243,45 +2839,17 @@

    -

    +

    __init__(paths, piece_length, progress=True) -

    +

    Generate hashes of piece length data from filelist contents.

    -
    - Source code in torrentfile\hasher.py -
    54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    def __init__(self, paths: list, piece_length: int, progress: bool = True):
    -    """Generate hashes of piece length data from filelist contents."""
    -    self.piece_length = piece_length
    -    self.paths = paths
    -    self.progress = progress
    -    self.total = sum([os.path.getsize(i) for i in self.paths])
    -    self.index = 0
    -    self.current = open(self.paths[0], "rb")
    -    if self.progress:
    -        total = os.path.getsize(self.paths[0])
    -        self.prog_start(total, self.paths[0], unit="bytes")
    -    logger.debug("Hashing %s", str(self.paths[0]))
    -
    -
    -
    @@ -6292,11 +2860,11 @@
    -
    +

    __iter__() -

    +
    @@ -6321,30 +2889,6 @@
    -
    - Source code in torrentfile\hasher.py -
    67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    def __iter__(self):
    -    """
    -    Iterate through feed pieces.
    -
    -    Returns
    -    -------
    -    self : iterator
    -        Iterator for leaves/hash pieces.
    -    """
    -    return self
    -
    -
    -
    @@ -6355,11 +2899,11 @@
    -
    +

    __next__() -

    +
    @@ -6384,52 +2928,6 @@
    -
    - Source code in torrentfile\hasher.py -
    123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    def __next__(self) -> bytes:
    -    """
    -    Generate piece-length pieces of data from input file list.
    -
    -    Returns
    -    -------
    -    bytes
    -        SHA1 hash of the piece extracted.
    -    """
    -    while True:
    -        piece = bytearray(self.piece_length)
    -        size = self.current.readinto(piece)
    -        if size == 0:
    -            if not self.next_file():
    -                raise StopIteration
    -        elif size < self.piece_length:
    -            self.prog_update(size)
    -            return self._handle_partial(piece[:size])
    -        else:
    -            self.prog_update(size)
    -            return sha1(piece).digest()  # nosec
    -
    -
    -
    @@ -6440,11 +2938,11 @@
    -
    +

    _handle_partial(arr) -

    +
    @@ -6493,56 +2991,6 @@
    -
    - Source code in torrentfile\hasher.py -
     78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    def _handle_partial(self, arr: bytearray) -> bytearray:
    -    """
    -    Define the handling partial pieces that span 2 or more files.
    -
    -    Parameters
    -    ----------
    -    arr : bytearray
    -        Incomplete piece containing partial data
    -
    -    Returns
    -    -------
    -    digest : bytearray
    -        SHA1 digest of the complete piece.
    -    """
    -    while len(arr) < self.piece_length and self.next_file():
    -        target = self.piece_length - len(arr)
    -        temp = bytearray(target)
    -        size = self.current.readinto(temp)
    -        arr.extend(temp[:size])
    -        self.prog_update(size)
    -        if size == target:
    -            break
    -    return sha1(arr).digest()  # nosec
    -
    -
    -
    @@ -6553,11 +3001,11 @@
    -
    +

    next_file() -

    +
    @@ -6582,50 +3030,6 @@
    -
    - Source code in torrentfile\hasher.py -
    102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    def next_file(self) -> bool:
    -    """
    -    Seemlessly transition to next file in file list.
    -
    -    Returns
    -    -------
    -    bool:
    -        True if there is a next file otherwise False.
    -    """
    -    self.index += 1
    -    self.prog_close()
    -    if self.index < len(self.paths):
    -        path = self.paths[self.index]
    -        logger.debug("Hashing %s", str(path))
    -        self.current.close()
    -        if self.progress:
    -            self.prog_start(os.path.getsize(path), path, unit="bytes")
    -        self.current = open(path, "rb")
    -        return True
    -    return False
    -
    -
    -

    @@ -6646,12 +3050,12 @@
    -

    +

    HasherHybrid -

    +
    @@ -6710,262 +3114,6 @@

    -
    - Source code in torrentfile\hasher.py -
    272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    class HasherHybrid(CbMixin, ProgMixin):
    -    """
    -    Calculate root and piece hashes for creating hybrid torrent file.
    -
    -    **DEPRECATED**
    -
    -    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
    -    With a branching factor of 2, merge layer hashes until blocks equal
    -    piece_length bytes for the piece layer, and then the root hash.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        path to target file.
    -    piece_length : int
    -        piece length for data chunks.
    -    progress : int
    -        default = None
    -    """
    -
    -    def __init__(self, path: str, piece_length: int, progress: bool = True):
    -        """
    -        Construct Hasher class instances for each file in torrent.
    -
    -        **DEPRECATED**
    -        """
    -        self.path = path
    -        self.piece_length = piece_length
    -        self.pieces = []
    -        self.layer_hashes = []
    -        self.piece_layer = None
    -        self.root = None
    -        self.padding_piece = None
    -        self.padding_file = None
    -        if progress:
    -            self.prog_start(os.path.getsize(path), path, unit="bytes")
    -        self.amount = piece_length // BLOCK_SIZE
    -        with open(path, "rb") as data:
    -            self.process_file(data)
    -
    -    def _pad_remaining(self, block_count: int):
    -        """
    -        Generate Hash sized, 0 filled bytes for padding.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        block_count : int
    -            current total number of blocks collected.
    -
    -        Returns
    -        -------
    -        padding : bytes
    -            Padding to fill remaining portion of tree.
    -        """
    -        # when the there is only one block for file
    -        remaining = self.amount - block_count
    -        if not self.layer_hashes:
    -            power2 = next_power_2(block_count)
    -            remaining = power2 - block_count
    -        self.prog_update(HASH_SIZE * remaining)
    -        return [bytes(HASH_SIZE) for _ in range(remaining)]
    -
    -    def process_file(self, data: bytearray):
    -        """
    -        Calculate layer hashes for contents of file.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        data : BytesIO
    -            File opened in read mode.
    -        """
    -        while True:
    -            plength = self.piece_length
    -            blocks = []
    -            piece = sha1()  # nosec
    -            total = 0
    -            block = bytearray(BLOCK_SIZE)
    -            for _ in range(self.amount):
    -                size = data.readinto(block)
    -                self.prog_update(size)
    -                if not size:
    -                    break
    -                total += size
    -                plength -= size
    -                blocks.append(sha256(block[:size]).digest())
    -                piece.update(block[:size])
    -            if not blocks:
    -                break
    -            if len(blocks) != self.amount:
    -                padding = self._pad_remaining(len(blocks))
    -                blocks.extend(padding)
    -            layer_hash = merkle_root(blocks)
    -            if self._cb:
    -                self._cb(layer_hash)
    -            self.layer_hashes.append(layer_hash)
    -            if plength > 0:
    -                self.padding_file = {
    -                    "attr": "p",
    -                    "length": plength,
    -                    "path": [".pad", str(plength)],
    -                }
    -                piece.update(bytes(plength))
    -            self.pieces.append(piece.digest())  # nosec
    -        self._calculate_root()
    -        self.prog_close()
    -
    -    def _calculate_root(self):
    -        """
    -        Calculate the root hash for opened file.
    -
    -        **DEPRECATED**
    -        """
    -        self.piece_layer = b"".join(self.layer_hashes)
    -
    -        if len(self.layer_hashes) > 1:
    -            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
    -
    -            pow2 = next_power_2(len(self.layer_hashes))
    -            remainder = pow2 - len(self.layer_hashes)
    -
    -            self.layer_hashes += [pad_piece for _ in range(remainder)]
    -        self.root = merkle_root(self.layer_hashes)
    -
    -
    -
    @@ -6991,11 +3139,11 @@

    -

    +

    __init__(path, piece_length, progress=True) -

    +
    @@ -7003,48 +3151,6 @@

    Construct Hasher class instances for each file in torrent.

    DEPRECATED

    -
    - Source code in torrentfile\hasher.py -
    292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    def __init__(self, path: str, piece_length: int, progress: bool = True):
    -    """
    -    Construct Hasher class instances for each file in torrent.
    -
    -    **DEPRECATED**
    -    """
    -    self.path = path
    -    self.piece_length = piece_length
    -    self.pieces = []
    -    self.layer_hashes = []
    -    self.piece_layer = None
    -    self.root = None
    -    self.padding_piece = None
    -    self.padding_file = None
    -    if progress:
    -        self.prog_start(os.path.getsize(path), path, unit="bytes")
    -    self.amount = piece_length // BLOCK_SIZE
    -    with open(path, "rb") as data:
    -        self.process_file(data)
    -
    -
    -
    @@ -7055,11 +3161,11 @@
    -
    +

    _calculate_root() -

    +
    @@ -7067,42 +3173,6 @@
    Calculate the root hash for opened file.

    DEPRECATED

    -
    - Source code in torrentfile\hasher.py -
    382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    def _calculate_root(self):
    -    """
    -    Calculate the root hash for opened file.
    -
    -    **DEPRECATED**
    -    """
    -    self.piece_layer = b"".join(self.layer_hashes)
    -
    -    if len(self.layer_hashes) > 1:
    -        pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
    -
    -        pow2 = next_power_2(len(self.layer_hashes))
    -        remainder = pow2 - len(self.layer_hashes)
    -
    -        self.layer_hashes += [pad_piece for _ in range(remainder)]
    -    self.root = merkle_root(self.layer_hashes)
    -
    -
    -
    @@ -7113,11 +3183,11 @@
    +

    _pad_remaining(block_count) -

    +
    @@ -7167,56 +3237,6 @@
    -
    - Source code in torrentfile\hasher.py -
    312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    def _pad_remaining(self, block_count: int):
    -    """
    -    Generate Hash sized, 0 filled bytes for padding.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    block_count : int
    -        current total number of blocks collected.
    -
    -    Returns
    -    -------
    -    padding : bytes
    -        Padding to fill remaining portion of tree.
    -    """
    -    # when the there is only one block for file
    -    remaining = self.amount - block_count
    -    if not self.layer_hashes:
    -        power2 = next_power_2(block_count)
    -        remaining = power2 - block_count
    -    self.prog_update(HASH_SIZE * remaining)
    -    return [bytes(HASH_SIZE) for _ in range(remaining)]
    -
    -
    -
    @@ -7227,11 +3247,11 @@
    -
    +

    process_file(data) -

    +
    @@ -7263,100 +3283,6 @@
    -
    - Source code in torrentfile\hasher.py -
    336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    def process_file(self, data: bytearray):
    -    """
    -    Calculate layer hashes for contents of file.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    data : BytesIO
    -        File opened in read mode.
    -    """
    -    while True:
    -        plength = self.piece_length
    -        blocks = []
    -        piece = sha1()  # nosec
    -        total = 0
    -        block = bytearray(BLOCK_SIZE)
    -        for _ in range(self.amount):
    -            size = data.readinto(block)
    -            self.prog_update(size)
    -            if not size:
    -                break
    -            total += size
    -            plength -= size
    -            blocks.append(sha256(block[:size]).digest())
    -            piece.update(block[:size])
    -        if not blocks:
    -            break
    -        if len(blocks) != self.amount:
    -            padding = self._pad_remaining(len(blocks))
    -            blocks.extend(padding)
    -        layer_hash = merkle_root(blocks)
    -        if self._cb:
    -            self._cb(layer_hash)
    -        self.layer_hashes.append(layer_hash)
    -        if plength > 0:
    -            self.padding_file = {
    -                "attr": "p",
    -                "length": plength,
    -                "path": [".pad", str(plength)],
    -            }
    -            piece.update(bytes(plength))
    -        self.pieces.append(piece.digest())  # nosec
    -    self._calculate_root()
    -    self.prog_close()
    -
    -
    -

    @@ -7377,12 +3303,12 @@
    -

    +

    HasherV2 -

    +
    @@ -7442,212 +3368,6 @@

    -
    - Source code in torrentfile\hasher.py -
    169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    class HasherV2(CbMixin, ProgMixin):
    -    """
    -    Calculate the root hash and piece layers for file contents.
    -
    -    **DEPRECATED**
    -
    -    Iterates over 16KiB blocks of data from given file, hashes the data,
    -    then creates a hash tree from the individual block hashes until size of
    -    hashed data equals the piece-length.  Then continues the hash tree until
    -    root hash is calculated.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to file.
    -    piece_length : int
    -        Size of layer hashes pieces.
    -    progress : int
    -        default = None
    -    """
    -
    -    def __init__(self, path: str, piece_length: int, progress: bool = True):
    -        """
    -        Calculate and store hash information for specific file.
    -
    -        **DEPRECATED**
    -        """
    -        self.path = path
    -        self.root = None
    -        self.piece_layer = None
    -        self.layer_hashes = []
    -        self.piece_length = piece_length
    -        self.num_blocks = piece_length // BLOCK_SIZE
    -        if progress:
    -            self.prog_start(os.path.getsize(path), path, unit="bytes")
    -        with open(self.path, "rb") as fd:
    -            self.process_file(fd)
    -
    -    def process_file(self, fd: str):
    -        """
    -        Calculate hashes over 16KiB chuncks of file content.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        fd : TextIOWrapper
    -            Opened file in read mode.
    -        """
    -        while True:
    -            blocks = []
    -            leaf = bytearray(BLOCK_SIZE)
    -            # generate leaves of merkle tree
    -
    -            for _ in range(self.num_blocks):
    -                size = fd.readinto(leaf)
    -                self.prog_update(size)
    -                if not size:
    -                    break
    -                blocks.append(sha256(leaf[:size]).digest())
    -
    -            # blocks is empty mean eof
    -            if not blocks:
    -                break
    -            if len(blocks) != self.num_blocks:
    -                # when size of file doesn't fill the last block
    -                # when the file contains multiple pieces
    -                remaining = self.num_blocks - len(blocks)
    -                if not self.layer_hashes:
    -                    # when the there is only one block for file
    -                    power2 = next_power_2(len(blocks))
    -                    remaining = power2 - len(blocks)
    -
    -                # pad the the rest with zeroes to fill remaining space.
    -                padding = [bytes(32) for _ in range(remaining)]
    -                self.prog_update(HASH_SIZE * remaining)
    -                blocks.extend(padding)
    -            # calculate the root hash for the merkle tree up to piece-length
    -
    -            layer_hash = merkle_root(blocks)
    -            if self._cb:
    -                self._cb(layer_hash)
    -            self.layer_hashes.append(layer_hash)
    -        self._calculate_root()
    -        self.prog_close()
    -
    -    def _calculate_root(self):
    -        """
    -        Calculate root hash for the target file.
    -
    -        **DEPRECATED**
    -        """
    -        self.piece_layer = b"".join(self.layer_hashes)
    -        hashes = len(self.layer_hashes)
    -        if hashes > 1:
    -            pow2 = next_power_2(hashes)
    -            remainder = pow2 - hashes
    -            pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)]
    -            for _ in range(remainder):
    -                self.layer_hashes.append(merkle_root(pad_piece))
    -        self.root = merkle_root(self.layer_hashes)
    -
    -
    -
    @@ -7670,11 +3390,11 @@

    -

    +

    __init__(path, piece_length, progress=True) -

    +
    @@ -7682,42 +3402,6 @@

    Calculate and store hash information for specific file.

    DEPRECATED

    -
    - Source code in torrentfile\hasher.py -
    190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    def __init__(self, path: str, piece_length: int, progress: bool = True):
    -    """
    -    Calculate and store hash information for specific file.
    -
    -    **DEPRECATED**
    -    """
    -    self.path = path
    -    self.root = None
    -    self.piece_layer = None
    -    self.layer_hashes = []
    -    self.piece_length = piece_length
    -    self.num_blocks = piece_length // BLOCK_SIZE
    -    if progress:
    -        self.prog_start(os.path.getsize(path), path, unit="bytes")
    -    with open(self.path, "rb") as fd:
    -        self.process_file(fd)
    -
    -
    -
    @@ -7728,11 +3412,11 @@
    -
    +

    _calculate_root() -

    +
    @@ -7740,40 +3424,6 @@

    Calculate root hash for the target file.

    DEPRECATED

    -
    - Source code in torrentfile\hasher.py -
    255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    def _calculate_root(self):
    -    """
    -    Calculate root hash for the target file.
    -
    -    **DEPRECATED**
    -    """
    -    self.piece_layer = b"".join(self.layer_hashes)
    -    hashes = len(self.layer_hashes)
    -    if hashes > 1:
    -        pow2 = next_power_2(hashes)
    -        remainder = pow2 - hashes
    -        pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)]
    -        for _ in range(remainder):
    -            self.layer_hashes.append(merkle_root(pad_piece))
    -    self.root = merkle_root(self.layer_hashes)
    -
    -
    -
    @@ -7784,11 +3434,11 @@
    -
    +

    process_file(fd) -

    +
    @@ -7820,104 +3470,6 @@
    -
    - Source code in torrentfile\hasher.py -
    207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    def process_file(self, fd: str):
    -    """
    -    Calculate hashes over 16KiB chuncks of file content.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    fd : TextIOWrapper
    -        Opened file in read mode.
    -    """
    -    while True:
    -        blocks = []
    -        leaf = bytearray(BLOCK_SIZE)
    -        # generate leaves of merkle tree
    -
    -        for _ in range(self.num_blocks):
    -            size = fd.readinto(leaf)
    -            self.prog_update(size)
    -            if not size:
    -                break
    -            blocks.append(sha256(leaf[:size]).digest())
    -
    -        # blocks is empty mean eof
    -        if not blocks:
    -            break
    -        if len(blocks) != self.num_blocks:
    -            # when size of file doesn't fill the last block
    -            # when the file contains multiple pieces
    -            remaining = self.num_blocks - len(blocks)
    -            if not self.layer_hashes:
    -                # when the there is only one block for file
    -                power2 = next_power_2(len(blocks))
    -                remaining = power2 - len(blocks)
    -
    -            # pad the the rest with zeroes to fill remaining space.
    -            padding = [bytes(32) for _ in range(remaining)]
    -            self.prog_update(HASH_SIZE * remaining)
    -            blocks.extend(padding)
    -        # calculate the root hash for the merkle tree up to piece-length
    -
    -        layer_hash = merkle_root(blocks)
    -        if self._cb:
    -            self._cb(layer_hash)
    -        self.layer_hashes.append(layer_hash)
    -    self._calculate_root()
    -    self.prog_close()
    -
    -
    -

    @@ -7939,11 +3491,11 @@
    -

    +

    merkle_root(blocks) -

    +
    @@ -7992,52 +3544,6 @@

    -
    - Source code in torrentfile\hasher.py -
    146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    def merkle_root(blocks: list) -> bytes:
    -    """
    -    Calculate the merkle root for a seq of sha256 hash digests.
    -
    -    Parameters
    -    ----------
    -    blocks : list
    -        a sequence of sha256 layer hashes.
    -
    -    Returns
    -    -------
    -    bytes
    -        the sha256 root hash of the merkle tree.
    -    """
    -    if blocks:
    -        while len(blocks) > 1:
    -            blocks = [
    -                sha256(x + y).digest() for x, y in zip(*[iter(blocks)] * 2)
    -            ]
    -        return blocks[0]
    -    return blocks
    -
    -
    -
    @@ -8058,12 +3564,12 @@

    -

    +

    interactive -

    +
    @@ -8083,12 +3589,12 @@

    -

    +

    InteractiveCreator -

    +
    @@ -8097,190 +3603,6 @@

    Class namespace for interactive program options.

    -
    - Source code in torrentfile\interactive.py -
    293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    class InteractiveCreator:
    -    """
    -    Class namespace for interactive program options.
    -    """
    -
    -    def __init__(self):
    -        """
    -        Initialize interactive meta file creator dialog.
    -        """
    -        self.kwargs = {
    -            "announce": None,
    -            "url_list": None,
    -            "private": None,
    -            "source": None,
    -            "comment": None,
    -            "piece_length": None,
    -            "outfile": None,
    -            "path": None,
    -            "httpseeds": None,
    -        }
    -        self.outfile, self.meta = self.get_props()
    -
    -    def get_props(self):
    -        """
    -        Gather details for torrentfile from user.
    -        """
    -        piece_length = get_input(
    -            "Piece Length (empty=auto): ", lambda x: x.isdigit()
    -        )
    -
    -        self.kwargs["piece_length"] = piece_length
    -        announce = get_input(
    -            "Tracker list (empty): ", lambda x: isinstance(x, str)
    -        )
    -
    -        if announce:
    -            self.kwargs["announce"] = announce.split()
    -
    -        url_list = get_input(
    -            "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str)
    -        )
    -
    -        httpseeds = get_input(
    -            "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str)
    -        )
    -
    -        if url_list:
    -            self.kwargs["url_list"] = url_list.split()
    -        if httpseeds:
    -            self.kwargs["httpseeds"] = httpseeds.split()
    -        comment = get_input("Comment (empty): ", None)
    -
    -        if comment:
    -            self.kwargs["comment"] = comment
    -        source = get_input("Source (empty): ", None)
    -
    -        if source:
    -            self.kwargs["source"] = source
    -
    -        private = get_input(
    -            "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN"
    -        )
    -
    -        if private and private.lower() == "y":
    -            self.kwargs["private"] = 1
    -
    -        contents = get_input("Content Path: ", os.path.exists)
    -        self.kwargs["path"] = contents
    -
    -        outfile = get_input(
    -            f"Output Path ({contents}.torrent): ",
    -            lambda x: os.path.exists(os.path.dirname(x)),
    -        )
    -
    -        if outfile:
    -            self.kwargs["outfile"] = outfile
    -
    -        meta_version = get_input(
    -            "Meta Version {1,2,3}: (1)", lambda x: x in "123"
    -        )
    -
    -        showcenter(f"creating {outfile}")
    -
    -        if meta_version == "3":
    -            torrent = TorrentFileHybrid(**self.kwargs)
    -        elif meta_version == "2":
    -            torrent = TorrentFileV2(**self.kwargs)
    -        else:
    -            torrent = TorrentFile(**self.kwargs)
    -        return torrent.write()
    -
    -
    -
    @@ -8298,53 +3620,17 @@

    -

    +

    __init__() -

    +

    Initialize interactive meta file creator dialog.

    -
    - Source code in torrentfile\interactive.py -
    298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    def __init__(self):
    -    """
    -    Initialize interactive meta file creator dialog.
    -    """
    -    self.kwargs = {
    -        "announce": None,
    -        "url_list": None,
    -        "private": None,
    -        "source": None,
    -        "comment": None,
    -        "piece_length": None,
    -        "outfile": None,
    -        "path": None,
    -        "httpseeds": None,
    -    }
    -    self.outfile, self.meta = self.get_props()
    -
    -
    -
    @@ -8355,157 +3641,17 @@
    +

    get_props() -

    +

    Gather details for torrentfile from user.

    -
    - Source code in torrentfile\interactive.py -
    315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    def get_props(self):
    -    """
    -    Gather details for torrentfile from user.
    -    """
    -    piece_length = get_input(
    -        "Piece Length (empty=auto): ", lambda x: x.isdigit()
    -    )
    -
    -    self.kwargs["piece_length"] = piece_length
    -    announce = get_input(
    -        "Tracker list (empty): ", lambda x: isinstance(x, str)
    -    )
    -
    -    if announce:
    -        self.kwargs["announce"] = announce.split()
    -
    -    url_list = get_input(
    -        "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str)
    -    )
    -
    -    httpseeds = get_input(
    -        "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str)
    -    )
    -
    -    if url_list:
    -        self.kwargs["url_list"] = url_list.split()
    -    if httpseeds:
    -        self.kwargs["httpseeds"] = httpseeds.split()
    -    comment = get_input("Comment (empty): ", None)
    -
    -    if comment:
    -        self.kwargs["comment"] = comment
    -    source = get_input("Source (empty): ", None)
    -
    -    if source:
    -        self.kwargs["source"] = source
    -
    -    private = get_input(
    -        "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN"
    -    )
    -
    -    if private and private.lower() == "y":
    -        self.kwargs["private"] = 1
    -
    -    contents = get_input("Content Path: ", os.path.exists)
    -    self.kwargs["path"] = contents
    -
    -    outfile = get_input(
    -        f"Output Path ({contents}.torrent): ",
    -        lambda x: os.path.exists(os.path.dirname(x)),
    -    )
    -
    -    if outfile:
    -        self.kwargs["outfile"] = outfile
    -
    -    meta_version = get_input(
    -        "Meta Version {1,2,3}: (1)", lambda x: x in "123"
    -    )
    -
    -    showcenter(f"creating {outfile}")
    -
    -    if meta_version == "3":
    -        torrent = TorrentFileHybrid(**self.kwargs)
    -    elif meta_version == "2":
    -        torrent = TorrentFileV2(**self.kwargs)
    -    else:
    -        torrent = TorrentFile(**self.kwargs)
    -    return torrent.write()
    -
    -
    -
    @@ -8526,12 +3672,12 @@
    +

    InteractiveEditor -

    +
    @@ -8540,212 +3686,6 @@

    Interactive dialog class for torrent editing.

    -
    - Source code in torrentfile\interactive.py -
    190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    class InteractiveEditor:
    -    """
    -    Interactive dialog class for torrent editing.
    -    """
    -
    -    def __init__(self, metafile: str):
    -        """
    -        Initialize the Interactive torrent editor guide.
    -
    -        Parameters
    -        ----------
    -        metafile : str
    -            user input string identifying the path to a torrent meta file.
    -        """
    -        self.metafile = metafile
    -        self.meta = pyben.load(metafile)
    -        self.info = self.meta["info"]
    -
    -        self.args = {
    -            "url-list": self.meta.get("url-list", None),
    -            "httpseeds": self.meta.get("httpseeds", None),
    -            "announce": self.meta.get("announce-list", None),
    -            "source": self.info.get("source", None),
    -            "private": self.info.get("private", None),
    -            "comment": self.info.get("comment", None),
    -        }
    -
    -    def show_current(self):
    -        """
    -        Display the current met file information to screen.
    -        """
    -        out = "Current properties and values:\n"
    -        longest = max([len(label) for label in self.args]) + 3
    -        for key, val in self.args.items():
    -            txt = (key.title() + ":").ljust(longest) + str(val)
    -            out += f"\t{txt}\n"
    -        showtext(out)
    -
    -    def sanatize_response(self, key, response):
    -        """
    -        Convert the input data into a form recognizable by the program.
    -
    -        Parameters
    -        ----------
    -        key : str
    -            name of the property and attribute being eddited.
    -        response : str
    -            User input value the property is being edited to.
    -        """
    -        if key in ["announce", "url-list", "httpseeds"]:
    -            val = response.split()
    -        else:
    -            val = response
    -        self.args[key] = val
    -
    -    def edit_props(self):
    -        """
    -        Loop continuosly for edits until user signals DONE.
    -        """
    -        while True:
    -            showcenter(
    -                "Choose the number for a propert the needs editing."
    -                "Enter DONE when all editing has been completed."
    -            )
    -
    -            props = {
    -                1: "comment",
    -                2: "source",
    -                3: "private",
    -                4: "tracker",
    -                5: "web-seed",
    -                6: "httpseeds",
    -            }
    -
    -            args = {
    -                1: "comment",
    -                2: "source",
    -                3: "private",
    -                4: "announce",
    -                5: "url-list",
    -                6: "httpseeds",
    -            }
    -
    -            txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
    -            prop = get_input(txt)
    -            if prop.lower() == "done":
    -                break
    -
    -            if prop.isdigit() and 0 < int(prop) < 6:
    -                key = props[int(prop)]
    -                key2 = args[int(prop)]
    -                val = self.args.get(key2)
    -                showtext(
    -                    "Enter new property value or leave empty for no value."
    -                )
    -                response = get_input(f"{key.title()} ({val}): ")
    -                self.sanatize_response(key2, response)
    -
    -            else:
    -                showtext("Invalid input: Try again.")
    -        edit_torrent(self.metafile, self.args)
    -
    -
    -
    @@ -8766,11 +3706,11 @@

    -

    +

    __init__(metafile) -

    +
    @@ -8801,52 +3741,6 @@
    - Source code in torrentfile\interactive.py -
    195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    def __init__(self, metafile: str):
    -    """
    -    Initialize the Interactive torrent editor guide.
    -
    -    Parameters
    -    ----------
    -    metafile : str
    -        user input string identifying the path to a torrent meta file.
    -    """
    -    self.metafile = metafile
    -    self.meta = pyben.load(metafile)
    -    self.info = self.meta["info"]
    -
    -    self.args = {
    -        "url-list": self.meta.get("url-list", None),
    -        "httpseeds": self.meta.get("httpseeds", None),
    -        "announce": self.meta.get("announce-list", None),
    -        "source": self.info.get("source", None),
    -        "private": self.info.get("private", None),
    -        "comment": self.info.get("comment", None),
    -    }
    -
    -
    -
    @@ -8857,113 +3751,17 @@
    +

    edit_props() -

    +

    Loop continuosly for edits until user signals DONE.

    -
    - Source code in torrentfile\interactive.py -
    245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    def edit_props(self):
    -    """
    -    Loop continuosly for edits until user signals DONE.
    -    """
    -    while True:
    -        showcenter(
    -            "Choose the number for a propert the needs editing."
    -            "Enter DONE when all editing has been completed."
    -        )
    -
    -        props = {
    -            1: "comment",
    -            2: "source",
    -            3: "private",
    -            4: "tracker",
    -            5: "web-seed",
    -            6: "httpseeds",
    -        }
    -
    -        args = {
    -            1: "comment",
    -            2: "source",
    -            3: "private",
    -            4: "announce",
    -            5: "url-list",
    -            6: "httpseeds",
    -        }
    -
    -        txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
    -        prop = get_input(txt)
    -        if prop.lower() == "done":
    -            break
    -
    -        if prop.isdigit() and 0 < int(prop) < 6:
    -            key = props[int(prop)]
    -            key2 = args[int(prop)]
    -            val = self.args.get(key2)
    -            showtext(
    -                "Enter new property value or leave empty for no value."
    -            )
    -            response = get_input(f"{key.title()} ({val}): ")
    -            self.sanatize_response(key2, response)
    -
    -        else:
    -            showtext("Invalid input: Try again.")
    -    edit_torrent(self.metafile, self.args)
    -
    -
    -
    @@ -8974,11 +3772,11 @@
    +

    sanatize_response(key, response) -

    +
    @@ -9019,42 +3817,6 @@
    - Source code in torrentfile\interactive.py -
    228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    def sanatize_response(self, key, response):
    -    """
    -    Convert the input data into a form recognizable by the program.
    -
    -    Parameters
    -    ----------
    -    key : str
    -        name of the property and attribute being eddited.
    -    response : str
    -        User input value the property is being edited to.
    -    """
    -    if key in ["announce", "url-list", "httpseeds"]:
    -        val = response.split()
    -    else:
    -        val = response
    -    self.args[key] = val
    -
    -
    -
    @@ -9065,41 +3827,17 @@
    +

    show_current() -

    +

    Display the current met file information to screen.

    -
    - Source code in torrentfile\interactive.py -
    217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    def show_current(self):
    -    """
    -    Display the current met file information to screen.
    -    """
    -    out = "Current properties and values:\n"
    -    longest = max([len(label) for label in self.args]) + 3
    -    for key, val in self.args.items():
    -        txt = (key.title() + ":").ljust(longest) + str(val)
    -        out += f"\t{txt}\n"
    -    showtext(out)
    -
    -
    -
    @@ -9121,11 +3859,11 @@
    +

    _get_input(txt) -

    +
    @@ -9174,42 +3912,6 @@

    -
    - Source code in torrentfile\interactive.py -
    53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    def _get_input(txt: str):  # pragma: no cover
    -    """
    -    Gather information needed from user.
    -
    -    Parameters
    -    ----------
    -    txt : str
    -        The message usually containing instructions for the user.
    -
    -    Returns
    -    -------
    -    str
    -        The text input received from the user.
    -    """
    -    value = input(txt)
    -    return value
    -
    -
    -

    @@ -9220,11 +3922,11 @@

    -

    +

    _get_input_loop(txt, func) -

    +
    @@ -9283,56 +3985,6 @@

    -
    - Source code in torrentfile\interactive.py -
    71
    -72
    -73
    -74
    -75
    -76
    -77
    -78
    -79
    -80
    -81
    -82
    -83
    -84
    -85
    -86
    -87
    -88
    -89
    -90
    -91
    -92
    -93
    def _get_input_loop(txt, func):  # pragma: no cover
    -    """
    -    Gather information needed from user.
    -
    -    Parameters
    -    ----------
    -    txt : str
    -        The message usually containing instructions for the user.
    -    func : function
    -        Validate/Check user input data, failure = retry, success = continue.
    -
    -    Returns
    -    -------
    -    str
    -        The text input received from the user.
    -    """
    -    while True:
    -        value = input(txt)
    -        if func and func(value):
    -            return value
    -        if not func or value == "":
    -            return value
    -        showtext(f"Invalid input {value}: try again")
    -
    -
    -
    @@ -9343,49 +3995,17 @@

    -

    +

    create_torrent() -

    +

    Create new torrent file interactively.

    -
    - Source code in torrentfile\interactive.py -
    163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    def create_torrent():
    -    """
    -    Create new torrent file interactively.
    -    """
    -    showcenter("Create Torrent")
    -    showtext(
    -        "\nEnter values for each of the options for the torrent creator, "
    -        "or leave blank for program defaults.\nSpaces are considered item "
    -        "seperators for options that accept a list of values.\nValues "
    -        "enclosed in () indicate the default value, while {} holds all "
    -        "valid choices available for the option.\n\n"
    -    )
    -    creator = InteractiveCreator()
    -    return creator
    -
    -
    -
    @@ -9396,39 +4016,17 @@

    -

    +

    edit_action() -

    +

    Edit the editable values of the torrent meta file.

    -
    - Source code in torrentfile\interactive.py -
    179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    def edit_action():
    -    """
    -    Edit the editable values of the torrent meta file.
    -    """
    -    showcenter("Edit Torrent")
    -    metafile = get_input("Metafile(.torrent): ", os.path.exists)
    -    dialog = InteractiveEditor(metafile)
    -    dialog.show_current()
    -    dialog.edit_props()
    -
    -
    -
    @@ -9439,11 +4037,11 @@

    -

    +

    get_input(*args) -

    +
    @@ -9492,44 +4090,6 @@

    -
    - Source code in torrentfile\interactive.py -
    34
    -35
    -36
    -37
    -38
    -39
    -40
    -41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    def get_input(*args: tuple):  # pragma: no cover
    -    """
    -    Determine appropriate input function to call.
    -
    -    Parameters
    -    ----------
    -    *args : tuple
    -        Arbitrary number of args to pass to next function
    -
    -    Returns
    -    -------
    -    str
    -        The results of the function call.
    -    """
    -    if len(args) == 2:
    -        return _get_input_loop(*args)
    -    return _get_input(*args)
    -
    -
    -

    @@ -9540,51 +4100,17 @@

    -

    +

    recheck_torrent() -

    +

    Check torrent download completed percentage.

    -
    - Source code in torrentfile\interactive.py -
    146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    def recheck_torrent():
    -    """
    -    Check torrent download completed percentage.
    -    """
    -    showcenter("Check Torrent")
    -    msg = "Enter path to torrent contents, and corresponding torrent metafile."
    -    showtext(msg)
    -    metafile = get_input(
    -        "Conent Path (downloads/complete/torrentname):", os.path.exists
    -    )
    -    contents = get_input("Metafile (*.torrent): ", os.path.exists)
    -    checker = Checker(metafile, contents)
    -    results = checker.results()
    -    showtext(f"Completion for {metafile} is {results}%")
    -    return results
    -
    -
    -
    @@ -9595,63 +4121,17 @@

    -

    +

    select_action() -

    +

    Operate TorrentFile program interactively through terminal.

    -
    - Source code in torrentfile\interactive.py -
    123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    def select_action():
    -    """
    -    Operate TorrentFile program interactively through terminal.
    -    """
    -    showcenter("TorrentFile: Starting Interactive Mode")
    -    action = get_input(
    -        "Enter the action you wish to perform.\n"
    -        "Action ( Create (c) | Edit (e) | Recheck (r) ): "
    -    )
    -    action = action.lower()
    -
    -    if "create" in action or action == "c":
    -        return create_torrent()
    -
    -    if "check" in action or action == "r":
    -        return recheck_torrent()
    -
    -    if "edit" in action or action == "e":
    -        return edit_action()
    -    print("Unable to recognize input.  Please try again.")  # pragma: nocover
    -    return select_action()  # pragma: nocover
    -
    -
    -
    @@ -9662,11 +4142,11 @@

    -

    +

    showcenter(txt) -

    +
    @@ -9697,36 +4177,6 @@

    -
    - Source code in torrentfile\interactive.py -
    108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    def showcenter(txt: str):
    -    """
    -    Print text to screen in the center position of the terminal.
    -
    -    Parameters
    -    ----------
    -    txt : str
    -        the preformated message to send to stdout.
    -    """
    -    termlen = shutil.get_terminal_size().columns
    -    padding = " " * int(((termlen - len(txt)) / 2))
    -    string = "".join(["\n", padding, txt, "\n"])
    -    showtext(string)
    -
    -
    -
    @@ -9737,11 +4187,11 @@

    -

    +

    showtext(txt) -

    +
    @@ -9772,30 +4222,6 @@

    -
    - Source code in torrentfile\interactive.py -
     96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    def showtext(txt):
    -    """
    -    Print contents of txt to screen.
    -
    -    Parameters
    -    ----------
    -    txt : str
    -        text to print to terminal.
    -    """
    -    sys.stdout.write(txt)
    -
    -
    -

    @@ -9816,12 +4242,12 @@

    -

    +

    mixins -

    +
    @@ -9843,12 +4269,12 @@

    -

    +

    CbMixin -

    +
    @@ -9857,46 +4283,6 @@

    Mixin class to set a callback during hashing procedure.

    -
    - Source code in torrentfile\mixins.py -
    33
    -34
    -35
    -36
    -37
    -38
    -39
    -40
    -41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    class CbMixin:
    -    """
    -    Mixin class to set a callback during hashing procedure.
    -    """
    -
    -    _cb = None
    -
    -    @classmethod
    -    def set_callback(cls, func):
    -        """
    -        Assign a callback to the Hashing class.
    -
    -        Parameters
    -        ----------
    -        func : function
    -            the callback function
    -        """
    -        cls._cb = func  # pragma: nocover
    -
    -
    -
    @@ -9914,14 +4300,14 @@

    -

    +

    set_callback(func) classmethod -

    +
    @@ -9952,32 +4338,6 @@
    -
    - Source code in torrentfile\mixins.py -
    40
    -41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    @classmethod
    -def set_callback(cls, func):
    -    """
    -    Assign a callback to the Hashing class.
    -
    -    Parameters
    -    ----------
    -    func : function
    -        the callback function
    -    """
    -    cls._cb = func  # pragma: nocover
    -
    -
    -
    @@ -9998,12 +4358,12 @@
    -

    +

    ProgMixin -

    +
    @@ -10012,176 +4372,12 @@

    Progress bar mixin class.

    Displays progress of hashing individual files, usefull when hashing really big files.

    -

    Methods
    +
    Methods

    prog_start prog_update prog_close

    -
    - Source code in torrentfile\mixins.py -
    131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    class ProgMixin:
    -    """
    -    Progress bar mixin class.
    -
    -    Displays progress of hashing individual files, usefull when hashing
    -    really big files.
    -
    -    Methods
    -    -------
    -    prog_start
    -    prog_update
    -    prog_close
    -    """
    -
    -    def prog_start(self, total, path, length=50, unit=None):
    -        """
    -        Generate a new progress bar for the given file path.
    -
    -        Parameters
    -        ----------
    -        total : int
    -            the total amount of units accumulating towards.
    -        path : str
    -            path to file being hashed.
    -        length : int
    -            the number of characters of the actual progress bar.
    -        unit : str
    -            the text representation of the value being measured.
    -        """
    -        title = path
    -        width = shutil.get_terminal_size().columns
    -        if len(str(title)) >= width // 2:
    -            parts = list(Path(title).parts)
    -            while len("//".join(parts)) > width // 2 and len(parts) > 0:
    -                del parts[0]
    -            title = os.path.join(*parts)
    -        length = min(length, width // 2)
    -        start = width - int(length * 1.5)
    -        self.prog = ProgressBar(total, title, length, unit, start)
    -
    -    def prog_update(self, val):
    -        """
    -        Update progress bar with given amount of progress.
    -
    -        Parameters
    -        ----------
    -        val : int
    -            the number of bytes count the progress bar should increase.
    -        """
    -        if self.is_active():
    -            self.prog.increment(val)
    -            pbar = self.prog.pbar()
    -            output = f"{self.prog.prefix}{pbar}{self.prog.suffix}\r"
    -            sys.stdout.write(output)
    -            sys.stdout.flush()
    -
    -    def prog_close(self):
    -        """
    -        Finalize the last bits of progress bar.
    -
    -        Increment the terminal by one line leaving the progress bar in place,
    -        and deleting the progress bar object to clear a space for the next one.
    -        """
    -        if self.is_active():
    -            sys.stdout.flush()
    -            sys.stdout.write("\n")
    -            del self.prog
    -
    -    def is_active(self):
    -        """
    -        Test to see if there is an active progress bar for object.
    -
    -        Returns
    -        -------
    -        bool :
    -            True if there is, otherwise False.
    -        """
    -        if hasattr(self, "prog"):
    -            return True
    -        return False
    -
    -
    -
    @@ -10198,11 +4394,11 @@
    Methods
    -
    +

    is_active() -

    +
    @@ -10226,34 +4422,6 @@
    -
    - Source code in torrentfile\mixins.py -
    199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    def is_active(self):
    -    """
    -    Test to see if there is an active progress bar for object.
    -
    -    Returns
    -    -------
    -    bool :
    -        True if there is, otherwise False.
    -    """
    -    if hasattr(self, "prog"):
    -        return True
    -    return False
    -
    -
    -

    @@ -10264,11 +4432,11 @@
    -
    +

    prog_close() -

    +
    @@ -10277,32 +4445,6 @@

    Increment the terminal by one line leaving the progress bar in place, and deleting the progress bar object to clear a space for the next one.

    -
    - Source code in torrentfile\mixins.py -
    187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    def prog_close(self):
    -    """
    -    Finalize the last bits of progress bar.
    -
    -    Increment the terminal by one line leaving the progress bar in place,
    -    and deleting the progress bar object to clear a space for the next one.
    -    """
    -    if self.is_active():
    -        sys.stdout.flush()
    -        sys.stdout.write("\n")
    -        del self.prog
    -
    -
    -
    @@ -10313,11 +4455,11 @@
    -
    +

    prog_start(total, path, length=50, unit=None) -

    +
    @@ -10378,60 +4520,6 @@
    -
    - Source code in torrentfile\mixins.py -
    145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    def prog_start(self, total, path, length=50, unit=None):
    -    """
    -    Generate a new progress bar for the given file path.
    -
    -    Parameters
    -    ----------
    -    total : int
    -        the total amount of units accumulating towards.
    -    path : str
    -        path to file being hashed.
    -    length : int
    -        the number of characters of the actual progress bar.
    -    unit : str
    -        the text representation of the value being measured.
    -    """
    -    title = path
    -    width = shutil.get_terminal_size().columns
    -    if len(str(title)) >= width // 2:
    -        parts = list(Path(title).parts)
    -        while len("//".join(parts)) > width // 2 and len(parts) > 0:
    -            del parts[0]
    -        title = os.path.join(*parts)
    -    length = min(length, width // 2)
    -    start = width - int(length * 1.5)
    -    self.prog = ProgressBar(total, title, length, unit, start)
    -
    -
    -

    @@ -10442,11 +4530,11 @@
    -
    +

    prog_update(val) -

    +
    @@ -10477,40 +4565,6 @@
    -
    - Source code in torrentfile\mixins.py -
    171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    def prog_update(self, val):
    -    """
    -    Update progress bar with given amount of progress.
    -
    -    Parameters
    -    ----------
    -    val : int
    -        the number of bytes count the progress bar should increase.
    -    """
    -    if self.is_active():
    -        self.prog.increment(val)
    -        pbar = self.prog.pbar()
    -        output = f"{self.prog.prefix}{pbar}{self.prog.suffix}\r"
    -        sys.stdout.write(output)
    -        sys.stdout.flush()
    -
    -
    -

    @@ -10531,12 +4585,12 @@
    -

    +

    ProgressBar -

    +
    @@ -10599,162 +4653,6 @@

    -
    - Source code in torrentfile\mixins.py -
     53
    - 54
    - 55
    - 56
    - 57
    - 58
    - 59
    - 60
    - 61
    - 62
    - 63
    - 64
    - 65
    - 66
    - 67
    - 68
    - 69
    - 70
    - 71
    - 72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    class ProgressBar:
    -    """
    -    Holds the state and details of the terminal progress bars.
    -
    -    Parameters
    -    ----------
    -    total : int
    -        the total amount to be accumulated.
    -    title : str
    -        the subject of the progress tracker
    -    length : int
    -        the width of the progress bar
    -    unit : str
    -        the text representation incremented
    -    """
    -
    -    def __init__(self, total, title, length, unit, start):
    -        """
    -        Construct the progress bar object and store state of it's properties.
    -        """
    -        self.total = total
    -        self.start = start
    -        self.length = length
    -        self.fill = chr(9608)
    -        self.empty = chr(9617)
    -        self.state = 0
    -        self.unit = unit
    -        self.show_total = total
    -        if not unit:
    -            self.unit = ""  # pragma: nocover
    -        elif unit == "bytes":
    -            if self.total > 10000000:
    -                self.show_total = math.floor(self.total / 1048576)
    -                self.unit = "MiB"
    -            elif self.total > 10000:
    -                self.show_total = math.floor(self.total / 1024)
    -                self.unit = "KiB"
    -        self.suffix = f"/{self.show_total} {self.unit}"
    -        if len(title) > start:
    -            title = title[: start - 1]
    -        padding = (start - len(title)) * " "
    -        self.prefix = "".join([title, padding])
    -
    -    def increment(self, value):
    -        """
    -        Increase the state of the progress bar value.
    -
    -        Parameters
    -        ----------
    -        value : int
    -            the amount to increment the state by.
    -        """
    -        self.state += value
    -
    -    def pbar(self):
    -        """
    -        Return the size of the filled portion of the progress bar.
    -
    -        Returns
    -        -------
    -        str :
    -            the progress bar characters
    -        """
    -        if self.state >= self.total:
    -            fill = self.length
    -        else:
    -            fill = math.ceil((self.state / self.total) * self.length)
    -        empty = self.length - fill
    -        if self.unit == "MiB":
    -            state = math.floor(self.state / 1048576)
    -        elif self.unit == "KiB":
    -            state = math.floor(self.state / 1024)
    -        else:
    -            state = self.state
    -        progbar = ["[", self.fill * fill, self.empty * empty, "] ", str(state)]
    -        return "".join(progbar)
    -
    -
    -
    @@ -10781,73 +4679,17 @@

    -

    +

    __init__(total, title, length, unit, start) -

    +

    Construct the progress bar object and store state of it's properties.

    -
    - Source code in torrentfile\mixins.py -
    69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    -77
    -78
    -79
    -80
    -81
    -82
    -83
    -84
    -85
    -86
    -87
    -88
    -89
    -90
    -91
    -92
    -93
    -94
    def __init__(self, total, title, length, unit, start):
    -    """
    -    Construct the progress bar object and store state of it's properties.
    -    """
    -    self.total = total
    -    self.start = start
    -    self.length = length
    -    self.fill = chr(9608)
    -    self.empty = chr(9617)
    -    self.state = 0
    -    self.unit = unit
    -    self.show_total = total
    -    if not unit:
    -        self.unit = ""  # pragma: nocover
    -    elif unit == "bytes":
    -        if self.total > 10000000:
    -            self.show_total = math.floor(self.total / 1048576)
    -            self.unit = "MiB"
    -        elif self.total > 10000:
    -            self.show_total = math.floor(self.total / 1024)
    -            self.unit = "KiB"
    -    self.suffix = f"/{self.show_total} {self.unit}"
    -    if len(title) > start:
    -        title = title[: start - 1]
    -    padding = (start - len(title)) * " "
    -    self.prefix = "".join([title, padding])
    -
    -
    -
    @@ -10858,11 +4700,11 @@
    -
    +

    increment(value) -

    +
    @@ -10893,30 +4735,6 @@
    -
    - Source code in torrentfile\mixins.py -
     96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    def increment(self, value):
    -    """
    -    Increase the state of the progress bar value.
    -
    -    Parameters
    -    ----------
    -    value : int
    -        the amount to increment the state by.
    -    """
    -    self.state += value
    -
    -
    -
    @@ -10927,11 +4745,11 @@
    -
    +

    pbar() -

    +
    @@ -10955,54 +4773,6 @@
    -
    - Source code in torrentfile\mixins.py -
    107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    def pbar(self):
    -    """
    -    Return the size of the filled portion of the progress bar.
    -
    -    Returns
    -    -------
    -    str :
    -        the progress bar characters
    -    """
    -    if self.state >= self.total:
    -        fill = self.length
    -    else:
    -        fill = math.ceil((self.state / self.total) * self.length)
    -    empty = self.length - fill
    -    if self.unit == "MiB":
    -        state = math.floor(self.state / 1048576)
    -    elif self.unit == "KiB":
    -        state = math.floor(self.state / 1024)
    -    else:
    -        state = self.state
    -    progbar = ["[", self.fill * fill, self.empty * empty, "] ", str(state)]
    -    return "".join(progbar)
    -
    -
    -
    @@ -11024,11 +4794,11 @@
    -

    +

    waiting(msg, flag, timeout=180) -

    +
    @@ -11079,90 +4849,6 @@

    -
    - Source code in torrentfile\mixins.py -
    213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    def waiting(msg, flag, timeout=180):
    -    """
    -    Show loading message while thread completes processing.
    -
    -    Parameters
    -    ----------
    -    msg : str
    -        Message string printed before the progress bar
    -    flag : list
    -        Once flag is filled exit loop
    -    timeout : int
    -        max amount of time to run the function.
    -    """
    -    then = time.time()
    -    codes, fill = list(range(9617, 9620)), chr(9619)
    -    size = idx = 0
    -    total = shutil.get_terminal_size().columns - len(msg) - 20
    -
    -    def output(text):
    -        """
    -        Print parameter message to the console.
    -
    -        Parameters
    -        ----------
    -        text : str
    -            output message
    -        """
    -        sys.stdout.write(text)
    -        sys.stdout.flush()
    -
    -    output("\n")
    -    while not flag:
    -        time.sleep(0.16)
    -        filled = (fill * size) + chr(codes[idx]) + (" " * (total - size))
    -        output(f"{msg}: {filled}\r")
    -        idx = idx + 1 if idx + 1 < len(codes) else 0
    -        size = size + 1 if size < total else 0
    -        if time.time() - then > timeout:
    -            break
    -    output("\n")
    -
    -
    -
    @@ -11183,12 +4869,12 @@

    -

    +

    recheck -

    +
    @@ -11217,12 +4903,12 @@

    -

    +

    Checker -

    +
    @@ -11266,7 +4952,7 @@

    -
    Example
    +
    Example
    >> metafile = "/path/to/torrentfile/content_file_or_dir.torrent"
     >> location = "/path/to/location"
     >> os.path.exists("/path/to/location/content_file_or_dir")
    @@ -11275,554 +4961,6 @@ 
    Example
    -
    - Source code in torrentfile\recheck.py -
     48
    - 49
    - 50
    - 51
    - 52
    - 53
    - 54
    - 55
    - 56
    - 57
    - 58
    - 59
    - 60
    - 61
    - 62
    - 63
    - 64
    - 65
    - 66
    - 67
    - 68
    - 69
    - 70
    - 71
    - 72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    class Checker(ProgMixin):
    -    """
    -    Check a given file or directory to see if it matches a torrentfile.
    -
    -    Public constructor for Checker class instance.
    -
    -    Parameters
    -    ----------
    -    metafile : str
    -        Path to ".torrent" file.
    -    path : str
    -        Path where the content is located in filesystem.
    -
    -    Example
    -    -------
    -        >> metafile = "/path/to/torrentfile/content_file_or_dir.torrent"
    -        >> location = "/path/to/location"
    -        >> os.path.exists("/path/to/location/content_file_or_dir")
    -        Out: True
    -        >> checker = Checker(metafile, location)
    -    """
    -
    -    _hook = None
    -
    -    def __init__(self, metafile: str, path: str):
    -        """
    -        Validate data against hashes contained in .torrent file.
    -
    -        Parameters
    -        ----------
    -        metafile : str
    -            path to .torrent file
    -        path : str
    -            path to content or contents parent directory.
    -        """
    -        if not os.path.exists(metafile):
    -            raise FileNotFoundError
    -        meta = []
    -        thread = Thread(target=pyben.loadinto, args=(metafile, meta))
    -        thread.start()
    -        self.last_log = None
    -        self.log_msg("Checking: %s, %s", metafile, path)
    -        self.metafile = metafile
    -        self.total = 0
    -        self.paths = []
    -        self.fileinfo = {}
    -        thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
    -        if not meta:  # pragma: nocover
    -            thread2.start()
    -            thread2.join()
    -        self.meta = meta[0]
    -        self.info = self.meta["info"]
    -        self.name = self.info["name"]
    -        self.piece_length = self.info["piece length"]
    -
    -        if "meta version" in self.info:
    -            if "pieces" in self.info:
    -                self.meta_version = 3
    -            else:
    -                self.meta_version = 2
    -        else:
    -            self.meta_version = 1
    -
    -        self.root = self.find_root(path)
    -        self.check_paths()
    -
    -    @classmethod
    -    def register_callback(cls, hook):
    -        """
    -        Register hooks from 3rd party programs to access generated info.
    -
    -        Parameters
    -        ----------
    -        hook : function
    -            callback function for the logging feature.
    -        """
    -        cls._hook = hook
    -
    -    def piece_checker(self):
    -        """
    -        Check individual pieces of the torrent.
    -
    -        Returns
    -        -------
    -        HashChecker | FeedChecker
    -            Individual piece hasher.
    -        """
    -        if self.meta_version == 1:
    -            return FeedChecker
    -        return HashChecker
    -
    -    def results(self):
    -        """
    -        Generate result percentage and store for future calls.
    -        """
    -        responses = []
    -        for response in self.iter_hashes():
    -            responses.append(response)
    -
    -        self.log_msg(
    -            "Final result for %s recheck:  %s", self.metafile, self._result
    -        )
    -        return self._result
    -
    -    def log_msg(self, *args, level=logging.INFO):
    -        """
    -        Log message `msg` to logger and send `msg` to callback hook.
    -
    -        Parameters
    -        ----------
    -        *args : dict
    -            formatting args for log message
    -        level : int
    -            Log level for this message; default=`logging.INFO`
    -        """
    -        message = args[0]
    -        if len(args) >= 3:
    -            message = message % tuple(args[1:])
    -        elif len(args) == 2:
    -            message = message % args[1]
    -
    -        # Repeat log messages should be ignored.
    -        if message != self.last_log:
    -            self.last_log = message
    -            logger.log(level, message)
    -            if self._hook and level == logging.INFO:
    -                self._hook(message)
    -
    -    def find_root(self, path: str) -> str:
    -        """
    -        Check path for torrent content.
    -
    -        The path can be a relative or absolute filesystem path.  In the case
    -        where the content is a single file, the path may point directly to the
    -        the file, or it may point to the parent directory.  If content points
    -        to a directory.  The directory will be checked to see if it matches
    -        the torrent's name, if not the directories contents will be searched.
    -        The returned value will be the absolute path that matches the torrent's
    -        name.
    -
    -        Parameters
    -        ----------
    -        path : str
    -            root path to torrent content
    -
    -        Returns
    -        -------
    -        str
    -            root path to content
    -        """
    -        if not os.path.exists(path):
    -            self.log_msg("Could not locate torrent content %s.", path)
    -            raise FileNotFoundError(path)
    -
    -        root = Path(path)
    -        if root.name == self.name:
    -            self.log_msg("Content found: %s.", str(root))
    -            return root
    -
    -        if self.name in os.listdir(root):
    -            return root / self.name
    -
    -        self.log_msg("Could not locate torrent content in: %s", str(root))
    -        raise FileNotFoundError(root)
    -
    -    def check_paths(self):
    -        """
    -        Gather all file paths described in the torrent file.
    -        """
    -        finfo = self.fileinfo
    -
    -        if "length" in self.info:
    -            self.log_msg("%s points to a single file", self.root)
    -            self.total = self.info["length"]
    -            self.paths.append(str(self.root))
    -
    -            finfo[0] = {
    -                "path": self.root,
    -                "length": self.info["length"],
    -            }
    -
    -            if self.meta_version > 1:
    -                root = self.info["file tree"][self.name][""]["pieces root"]
    -                finfo[0]["pieces root"] = root
    -
    -            return
    -
    -        # Otherwise Content is more than 1 file.
    -        self.log_msg("%s points to a directory", self.root)
    -        if self.meta_version == 1:
    -
    -            for i, item in enumerate(self.info["files"]):
    -                self.total += item["length"]
    -                base = os.path.join(*item["path"])
    -
    -                self.fileinfo[i] = {
    -                    "path": str(self.root / base),
    -                    "length": item["length"],
    -                }
    -
    -                self.paths.append(str(self.root / base))
    -            return
    -
    -        self.walk_file_tree(self.info["file tree"], [])
    -
    -    def walk_file_tree(self, tree: dict, partials: list):
    -        """
    -        Traverse File Tree dictionary to get file details.
    -
    -        Extract full pathnames, length, root hash, and layer hashes
    -        for each file included in the .torrent's file tree.
    -
    -        Parameters
    -        ----------
    -        tree : dict
    -            File Tree dict extracted from torrent file.
    -        partials : list
    -            list of intermediate pathnames.
    -        """
    -        for key, val in tree.items():
    -
    -            # Empty string means the tree's leaf is value
    -            if "" in val:
    -
    -                base = os.path.join(*partials, key)
    -                roothash = None
    -                length = val[""]["length"]
    -                roothash = None if not length else val[""]["pieces root"]
    -                full = str(self.root / base)
    -                self.fileinfo[len(self.paths)] = {
    -                    "path": full,
    -                    "length": length,
    -                    "pieces root": roothash,
    -                }
    -                self.paths.append(full)
    -                self.total += length
    -            else:
    -                self.walk_file_tree(val, partials + [key])
    -
    -    def iter_hashes(self) -> tuple:
    -        """
    -        Produce results of comparing torrent contents piece by piece.
    -
    -        Yields
    -        ------
    -        chunck : bytes
    -            hash of data found on disk
    -        piece : bytes
    -            hash of data when complete and correct
    -        path : str
    -            path to file being hashed
    -        size : int
    -            length of bytes hashed for piece
    -        """
    -        matched = consumed = 0
    -        checker = self.piece_checker()
    -        for chunk, piece, path, size in checker(self):
    -            consumed += size
    -            matching = 0
    -            if chunk == piece:
    -                matching += size
    -                matched += size
    -            yield chunk, piece, path, size
    -            total_consumed = str(int(consumed / self.total * 100))
    -            percent_matched = str(int(matched / consumed * 100))
    -            self.log_msg(
    -                "Processed: %s%%, Matched: %s%%",
    -                total_consumed,
    -                percent_matched,
    -                level=logging.DEBUG,
    -            )
    -        self._result = (matched / consumed) * 100 if consumed > 0 else 0
    -
    -
    -
    @@ -11851,11 +4989,11 @@
    Example
    -
    +

    __init__(metafile, path) -

    +
    @@ -11896,92 +5034,6 @@
    -
    - Source code in torrentfile\recheck.py -
     72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    def __init__(self, metafile: str, path: str):
    -    """
    -    Validate data against hashes contained in .torrent file.
    -
    -    Parameters
    -    ----------
    -    metafile : str
    -        path to .torrent file
    -    path : str
    -        path to content or contents parent directory.
    -    """
    -    if not os.path.exists(metafile):
    -        raise FileNotFoundError
    -    meta = []
    -    thread = Thread(target=pyben.loadinto, args=(metafile, meta))
    -    thread.start()
    -    self.last_log = None
    -    self.log_msg("Checking: %s, %s", metafile, path)
    -    self.metafile = metafile
    -    self.total = 0
    -    self.paths = []
    -    self.fileinfo = {}
    -    thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
    -    if not meta:  # pragma: nocover
    -        thread2.start()
    -        thread2.join()
    -    self.meta = meta[0]
    -    self.info = self.meta["info"]
    -    self.name = self.info["name"]
    -    self.piece_length = self.info["piece length"]
    -
    -    if "meta version" in self.info:
    -        if "pieces" in self.info:
    -            self.meta_version = 3
    -        else:
    -            self.meta_version = 2
    -    else:
    -        self.meta_version = 1
    -
    -    self.root = self.find_root(path)
    -    self.check_paths()
    -
    -
    -
    @@ -11992,99 +5044,17 @@
    -
    +

    check_paths() -

    +

    Gather all file paths described in the torrent file.

    -
    - Source code in torrentfile\recheck.py -
    213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    def check_paths(self):
    -    """
    -    Gather all file paths described in the torrent file.
    -    """
    -    finfo = self.fileinfo
    -
    -    if "length" in self.info:
    -        self.log_msg("%s points to a single file", self.root)
    -        self.total = self.info["length"]
    -        self.paths.append(str(self.root))
    -
    -        finfo[0] = {
    -            "path": self.root,
    -            "length": self.info["length"],
    -        }
    -
    -        if self.meta_version > 1:
    -            root = self.info["file tree"][self.name][""]["pieces root"]
    -            finfo[0]["pieces root"] = root
    -
    -        return
    -
    -    # Otherwise Content is more than 1 file.
    -    self.log_msg("%s points to a directory", self.root)
    -    if self.meta_version == 1:
    -
    -        for i, item in enumerate(self.info["files"]):
    -            self.total += item["length"]
    -            base = os.path.join(*item["path"])
    -
    -            self.fileinfo[i] = {
    -                "path": str(self.root / base),
    -                "length": item["length"],
    -            }
    -
    -            self.paths.append(str(self.root / base))
    -        return
    -
    -    self.walk_file_tree(self.info["file tree"], [])
    -
    -
    -
    @@ -12095,11 +5065,11 @@
    -
    +

    find_root(path) -

    +
    @@ -12155,82 +5125,6 @@
    -
    - Source code in torrentfile\recheck.py -
    176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    def find_root(self, path: str) -> str:
    -    """
    -    Check path for torrent content.
    -
    -    The path can be a relative or absolute filesystem path.  In the case
    -    where the content is a single file, the path may point directly to the
    -    the file, or it may point to the parent directory.  If content points
    -    to a directory.  The directory will be checked to see if it matches
    -    the torrent's name, if not the directories contents will be searched.
    -    The returned value will be the absolute path that matches the torrent's
    -    name.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        root path to torrent content
    -
    -    Returns
    -    -------
    -    str
    -        root path to content
    -    """
    -    if not os.path.exists(path):
    -        self.log_msg("Could not locate torrent content %s.", path)
    -        raise FileNotFoundError(path)
    -
    -    root = Path(path)
    -    if root.name == self.name:
    -        self.log_msg("Content found: %s.", str(root))
    -        return root
    -
    -    if self.name in os.listdir(root):
    -        return root / self.name
    -
    -    self.log_msg("Could not locate torrent content in: %s", str(root))
    -    raise FileNotFoundError(root)
    -
    -
    -
    @@ -12241,11 +5135,11 @@
    -
    +

    iter_hashes() -

    +
    @@ -12288,76 +5182,6 @@
    -
    - Source code in torrentfile\recheck.py -
    287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    def iter_hashes(self) -> tuple:
    -    """
    -    Produce results of comparing torrent contents piece by piece.
    -
    -    Yields
    -    ------
    -    chunck : bytes
    -        hash of data found on disk
    -    piece : bytes
    -        hash of data when complete and correct
    -    path : str
    -        path to file being hashed
    -    size : int
    -        length of bytes hashed for piece
    -    """
    -    matched = consumed = 0
    -    checker = self.piece_checker()
    -    for chunk, piece, path, size in checker(self):
    -        consumed += size
    -        matching = 0
    -        if chunk == piece:
    -            matching += size
    -            matched += size
    -        yield chunk, piece, path, size
    -        total_consumed = str(int(consumed / self.total * 100))
    -        percent_matched = str(int(matched / consumed * 100))
    -        self.log_msg(
    -            "Processed: %s%%, Matched: %s%%",
    -            total_consumed,
    -            percent_matched,
    -            level=logging.DEBUG,
    -        )
    -    self._result = (matched / consumed) * 100 if consumed > 0 else 0
    -
    -
    -
    @@ -12368,11 +5192,11 @@
    -
    +

    log_msg(*args, level=logging.INFO) -

    +
    @@ -12413,56 +5237,6 @@
    -
    - Source code in torrentfile\recheck.py -
    152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    def log_msg(self, *args, level=logging.INFO):
    -    """
    -    Log message `msg` to logger and send `msg` to callback hook.
    -
    -    Parameters
    -    ----------
    -    *args : dict
    -        formatting args for log message
    -    level : int
    -        Log level for this message; default=`logging.INFO`
    -    """
    -    message = args[0]
    -    if len(args) >= 3:
    -        message = message % tuple(args[1:])
    -    elif len(args) == 2:
    -        message = message % args[1]
    -
    -    # Repeat log messages should be ignored.
    -    if message != self.last_log:
    -        self.last_log = message
    -        logger.log(level, message)
    -        if self._hook and level == logging.INFO:
    -            self._hook(message)
    -
    -
    -
    @@ -12473,11 +5247,11 @@
    -
    +

    piece_checker() -

    +
    @@ -12502,34 +5276,6 @@
    -
    - Source code in torrentfile\recheck.py -
    126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    def piece_checker(self):
    -    """
    -    Check individual pieces of the torrent.
    -
    -    Returns
    -    -------
    -    HashChecker | FeedChecker
    -        Individual piece hasher.
    -    """
    -    if self.meta_version == 1:
    -        return FeedChecker
    -    return HashChecker
    -
    -
    -
    @@ -12540,14 +5286,14 @@
    -
    +

    register_callback(hook) classmethod -

    +
    @@ -12578,32 +5324,6 @@
    -
    - Source code in torrentfile\recheck.py -
    114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    @classmethod
    -def register_callback(cls, hook):
    -    """
    -    Register hooks from 3rd party programs to access generated info.
    -
    -    Parameters
    -    ----------
    -    hook : function
    -        callback function for the logging feature.
    -    """
    -    cls._hook = hook
    -
    -
    -
    @@ -12614,45 +5334,17 @@
    -
    +

    results() -

    +

    Generate result percentage and store for future calls.

    -
    - Source code in torrentfile\recheck.py -
    139
    -140
    -141
    -142
    -143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    def results(self):
    -    """
    -    Generate result percentage and store for future calls.
    -    """
    -    responses = []
    -    for response in self.iter_hashes():
    -        responses.append(response)
    -
    -    self.log_msg(
    -        "Final result for %s recheck:  %s", self.metafile, self._result
    -    )
    -    return self._result
    -
    -
    -
    @@ -12663,11 +5355,11 @@
    -
    +

    walk_file_tree(tree, partials) -

    +
    @@ -12710,76 +5402,6 @@
    -
    - Source code in torrentfile\recheck.py -
    253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    def walk_file_tree(self, tree: dict, partials: list):
    -    """
    -    Traverse File Tree dictionary to get file details.
    -
    -    Extract full pathnames, length, root hash, and layer hashes
    -    for each file included in the .torrent's file tree.
    -
    -    Parameters
    -    ----------
    -    tree : dict
    -        File Tree dict extracted from torrent file.
    -    partials : list
    -        list of intermediate pathnames.
    -    """
    -    for key, val in tree.items():
    -
    -        # Empty string means the tree's leaf is value
    -        if "" in val:
    -
    -            base = os.path.join(*partials, key)
    -            roothash = None
    -            length = val[""]["length"]
    -            roothash = None if not length else val[""]["pieces root"]
    -            full = str(self.root / base)
    -            self.fileinfo[len(self.paths)] = {
    -                "path": full,
    -                "length": length,
    -                "pieces root": roothash,
    -            }
    -            self.paths.append(full)
    -            self.total += length
    -        else:
    -            self.walk_file_tree(val, partials + [key])
    -
    -
    -
    @@ -12800,12 +5422,12 @@
    -

    +

    FeedChecker -

    +
    @@ -12842,316 +5464,6 @@

    -
    - Source code in torrentfile\recheck.py -
    322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    -403
    -404
    -405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    class FeedChecker(ProgMixin):
    -    """
    -    Validates torrent content.
    -
    -    Seemlesly validate torrent file contents by comparing hashes in
    -    metafile against data on disk.
    -
    -    Parameters
    -    ----------
    -    checker : object
    -        the checker class instance.
    -    """
    -
    -    def __init__(self, checker: Checker):
    -        """
    -        Generate hashes of piece length data from filelist contents.
    -        """
    -        self.piece_length = checker.piece_length
    -        self.paths = checker.paths
    -        self.pieces = checker.info["pieces"]
    -        self.fileinfo = checker.fileinfo
    -        self.piece_map = {}
    -        self.index = 0
    -        self.piece_count = 0
    -        self.it = None
    -
    -    def __iter__(self):
    -        """
    -        Assign iterator and return self.
    -        """
    -        self.it = self.iter_pieces()
    -        return self
    -
    -    def __next__(self):
    -        """
    -        Yield back result of comparison.
    -        """
    -        try:
    -            partial = next(self.it)
    -        except StopIteration as itererror:
    -            raise StopIteration from itererror
    -
    -        chunck = sha1(partial).digest()  # nosec
    -        start = self.piece_count * SHA1
    -        end = start + SHA1
    -        piece = self.pieces[start:end]
    -        self.piece_count += 1
    -        path = self.paths[self.index]
    -        return chunck, piece, path, len(partial)
    -
    -    def iter_pieces(self):
    -        """
    -        Iterate through, and hash pieces of torrent contents.
    -
    -        Yields
    -        ------
    -        piece : bytes
    -            hash digest for block of torrent data.
    -        """
    -        partial = bytearray()
    -        for i, path in enumerate(self.paths):
    -            total = self.fileinfo[i]["length"]
    -            self.prog_start(total, path, unit="bytes")
    -            self.index = i
    -            if os.path.exists(path):
    -                for piece in self.extract(path, partial):
    -                    self.prog_update(len(piece))
    -                    if (len(piece) == self.piece_length) or (
    -                        i + 1 == len(self.paths)
    -                    ):
    -                        yield piece
    -                    else:
    -                        partial = piece
    -
    -            else:
    -                length = self.fileinfo[i]["length"]
    -                for pad in self._gen_padding(partial, length):
    -                    if len(pad) == self.piece_length:
    -                        yield pad
    -                    else:
    -                        partial = pad
    -            self.prog_close()
    -
    -    def extract(self, path: str, partial: bytearray) -> bytearray:
    -        """
    -        Split file paths contents into blocks of data for hash pieces.
    -
    -        Parameters
    -        ----------
    -        path : str
    -            path to content.
    -        partial : bytes
    -            any remaining content from last file.
    -
    -        Returns
    -        -------
    -        bytearray
    -            Hash digest for block of .torrent contents.
    -        """
    -        read = 0
    -        length = self.fileinfo[self.index]["length"]
    -        partial = bytearray() if len(partial) == self.piece_length else partial
    -        if path not in self.paths:  # pragma: no cover
    -            raise MissingPathError(path)
    -        with open(path, "rb") as current:
    -            while True:
    -                bitlength = self.piece_length - len(partial)
    -                part = bytearray(bitlength)
    -                amount = current.readinto(part)
    -                read += amount
    -                partial.extend(part[:amount])
    -                if amount < bitlength:
    -                    if amount > 0 and read == length:
    -                        self.prog_update(amount)
    -                        yield partial
    -                    break
    -                self.prog_update(amount)
    -                yield partial
    -                partial = bytearray(0)
    -        if length != read:
    -            for pad in self._gen_padding(partial, length, read):
    -                yield pad
    -
    -    def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
    -        """
    -        Create padded pieces where file sizes do not match.
    -
    -        Parameters
    -        ----------
    -        partial : bytes
    -            any remaining data from last file processed.
    -        length : int
    -            size of space that needs padding
    -        read : int
    -            portion of length already padded
    -
    -        Yields
    -        ------
    -        bytes
    -            A piece length sized block of zeros.
    -        """
    -        while read < length:
    -            left = self.piece_length - len(partial)
    -            if length - read > left:
    -                padding = bytearray(left)
    -                partial.extend(padding)
    -                yield partial
    -                read += left
    -                partial = bytearray(0)
    -            else:
    -                partial.extend(bytearray(length - read))
    -                read = length
    -                yield partial
    -
    -
    -
    @@ -13176,45 +5488,17 @@

    -

    +

    __init__(checker) -

    +

    Generate hashes of piece length data from filelist contents.

    -
    - Source code in torrentfile\recheck.py -
    335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    def __init__(self, checker: Checker):
    -    """
    -    Generate hashes of piece length data from filelist contents.
    -    """
    -    self.piece_length = checker.piece_length
    -    self.paths = checker.paths
    -    self.pieces = checker.info["pieces"]
    -    self.fileinfo = checker.fileinfo
    -    self.piece_map = {}
    -    self.index = 0
    -    self.piece_count = 0
    -    self.it = None
    -
    -
    -
    @@ -13225,33 +5509,17 @@
    -
    +

    __iter__() -

    +

    Assign iterator and return self.

    -
    - Source code in torrentfile\recheck.py -
    348
    -349
    -350
    -351
    -352
    -353
    def __iter__(self):
    -    """
    -    Assign iterator and return self.
    -    """
    -    self.it = self.iter_pieces()
    -    return self
    -
    -
    -
    @@ -13262,53 +5530,17 @@
    -
    +

    __next__() -

    +

    Yield back result of comparison.

    -
    - Source code in torrentfile\recheck.py -
    355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    def __next__(self):
    -    """
    -    Yield back result of comparison.
    -    """
    -    try:
    -        partial = next(self.it)
    -    except StopIteration as itererror:
    -        raise StopIteration from itererror
    -
    -    chunck = sha1(partial).digest()  # nosec
    -    start = self.piece_count * SHA1
    -    end = start + SHA1
    -    piece = self.pieces[start:end]
    -    self.piece_count += 1
    -    path = self.paths[self.index]
    -    return chunck, piece, path, len(partial)
    -
    -
    -
    @@ -13319,11 +5551,11 @@
    -
    +

    _gen_padding(partial, length, read=0) -

    +
    @@ -13392,70 +5624,6 @@
    -
    - Source code in torrentfile\recheck.py -
    445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
    -    """
    -    Create padded pieces where file sizes do not match.
    -
    -    Parameters
    -    ----------
    -    partial : bytes
    -        any remaining data from last file processed.
    -    length : int
    -        size of space that needs padding
    -    read : int
    -        portion of length already padded
    -
    -    Yields
    -    ------
    -    bytes
    -        A piece length sized block of zeros.
    -    """
    -    while read < length:
    -        left = self.piece_length - len(partial)
    -        if length - read > left:
    -            padding = bytearray(left)
    -            partial.extend(padding)
    -            yield partial
    -            read += left
    -            partial = bytearray(0)
    -        else:
    -            partial.extend(bytearray(length - read))
    -            read = length
    -            yield partial
    -
    -
    -
    @@ -13466,11 +5634,11 @@
    -
    +

    extract(path, partial) -

    +
    @@ -13529,88 +5697,6 @@
    -
    - Source code in torrentfile\recheck.py -
    405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    def extract(self, path: str, partial: bytearray) -> bytearray:
    -    """
    -    Split file paths contents into blocks of data for hash pieces.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        path to content.
    -    partial : bytes
    -        any remaining content from last file.
    -
    -    Returns
    -    -------
    -    bytearray
    -        Hash digest for block of .torrent contents.
    -    """
    -    read = 0
    -    length = self.fileinfo[self.index]["length"]
    -    partial = bytearray() if len(partial) == self.piece_length else partial
    -    if path not in self.paths:  # pragma: no cover
    -        raise MissingPathError(path)
    -    with open(path, "rb") as current:
    -        while True:
    -            bitlength = self.piece_length - len(partial)
    -            part = bytearray(bitlength)
    -            amount = current.readinto(part)
    -            read += amount
    -            partial.extend(part[:amount])
    -            if amount < bitlength:
    -                if amount > 0 and read == length:
    -                    self.prog_update(amount)
    -                    yield partial
    -                break
    -            self.prog_update(amount)
    -            yield partial
    -            partial = bytearray(0)
    -    if length != read:
    -        for pad in self._gen_padding(partial, length, read):
    -            yield pad
    -
    -
    -
    @@ -13621,11 +5707,11 @@
    -
    +

    iter_pieces() -

    +
    @@ -13650,74 +5736,6 @@
    -
    - Source code in torrentfile\recheck.py -
    372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    -403
    def iter_pieces(self):
    -    """
    -    Iterate through, and hash pieces of torrent contents.
    -
    -    Yields
    -    ------
    -    piece : bytes
    -        hash digest for block of torrent data.
    -    """
    -    partial = bytearray()
    -    for i, path in enumerate(self.paths):
    -        total = self.fileinfo[i]["length"]
    -        self.prog_start(total, path, unit="bytes")
    -        self.index = i
    -        if os.path.exists(path):
    -            for piece in self.extract(path, partial):
    -                self.prog_update(len(piece))
    -                if (len(piece) == self.piece_length) or (
    -                    i + 1 == len(self.paths)
    -                ):
    -                    yield piece
    -                else:
    -                    partial = piece
    -
    -        else:
    -            length = self.fileinfo[i]["length"]
    -            for pad in self._gen_padding(partial, length):
    -                if len(pad) == self.piece_length:
    -                    yield pad
    -                else:
    -                    partial = pad
    -        self.prog_close()
    -
    -
    -
    @@ -13738,12 +5756,12 @@
    -

    +

    HashChecker -

    +
    @@ -13751,7 +5769,7 @@

    Bases: ProgMixin

    -

    Verify that root hashes of content files match the .torrent files.

    +

    Iterate through contents of meta data and verify with file contents.

    Parameters:

    @@ -13767,7 +5785,7 @@

    checker - Object + Checker

    the checker instance that maintains variables.

    @@ -13778,364 +5796,6 @@

    -
    - Source code in torrentfile\recheck.py -
    477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    -544
    -545
    -546
    -547
    -548
    -549
    -550
    -551
    -552
    -553
    -554
    -555
    -556
    -557
    -558
    -559
    -560
    -561
    -562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    -572
    -573
    -574
    -575
    -576
    -577
    -578
    -579
    -580
    -581
    -582
    -583
    -584
    -585
    -586
    -587
    -588
    -589
    -590
    -591
    -592
    -593
    -594
    -595
    -596
    -597
    -598
    -599
    -600
    -601
    -602
    -603
    -604
    -605
    -606
    -607
    -608
    -609
    -610
    -611
    -612
    -613
    -614
    -615
    -616
    -617
    -618
    -619
    -620
    -621
    -622
    -623
    -624
    -625
    -626
    -627
    -628
    -629
    -630
    -631
    -632
    -633
    -634
    -635
    -636
    -637
    -638
    -639
    -640
    -641
    -642
    -643
    -644
    -645
    -646
    -647
    -648
    -649
    -650
    -651
    -652
    -653
    class HashChecker(ProgMixin):
    -    """
    -    Verify that root hashes of content files match the .torrent files.
    -
    -    Parameters
    -    ----------
    -    checker : Object
    -        the checker instance that maintains variables.
    -    """
    -
    -    def __init__(self, checker: Checker):
    -        """
    -        Construct a HybridChecker instance.
    -        """
    -        self.checker = checker
    -        self.paths = checker.paths
    -        self.piece_length = checker.piece_length
    -        self.fileinfo = checker.fileinfo
    -        self.piece_layers = checker.meta["piece layers"]
    -        self.current = None
    -        self.index = -1
    -
    -    def __iter__(self):
    -        """
    -        Assign iterator and return self.
    -        """
    -        return self
    -
    -    def __next__(self):
    -        """
    -        Provide the result of comparison.
    -        """
    -        if self.current is None:
    -            self.next_file()
    -        try:
    -            return self.process_current()
    -        except StopIteration as itererr:
    -            if self.next_file():
    -                return self.process_current()
    -            raise StopIteration from itererr
    -
    -    class Padder:
    -        """
    -        Padding class to generate padding hashes wherever needed.
    -
    -        Parameters
    -        ----------
    -        length: int
    -            the total size of the mock file generating padding for.
    -        piece_length : int
    -            the block size that each hash represents.
    -        """
    -
    -        def __init__(self, length, piece_length):
    -            """
    -            Construct padding class to Mock missing or incomplete files.
    -
    -            Parameters
    -            ----------
    -            length : int
    -                size of the file
    -            piece_length : int
    -                the piece length for each iteration.
    -            """
    -            self.length = length
    -            self.piece_length = piece_length
    -            self.pad = sha256(bytearray(piece_length)).digest()
    -
    -        def __iter__(self):
    -            """
    -            Return self to correctly implement iterator type.
    -            """
    -            return self  # pragma: nocover
    -
    -        def __next__(self) -> bytes:
    -            """
    -            Iterate through seemingly endless sha256 hashes of zeros.
    -
    -            Returns
    -            -------
    -            tuple :
    -                returns the padding
    -
    -            Raises
    -            ------
    -            StopIteration
    -            """
    -            if self.length >= self.piece_length:
    -                self.length -= self.piece_length
    -                return self.pad
    -            if self.length > 0:
    -                pad = sha256(bytearray(self.length)).digest()
    -                self.length -= self.length
    -                return pad
    -            raise StopIteration
    -
    -    def next_file(self) -> bool:
    -        """
    -        Remove all references to  processed files and prepare for the next.
    -
    -        Returns
    -        -------
    -        bool
    -            if there is a next file found
    -        """
    -        self.index += 1
    -        self.prog_close()
    -        if self.current is None or self.index < len(self.paths):
    -            self.current = self.paths[self.index]
    -            self.length = self.fileinfo[self.index]["length"]
    -            self.root_hash = self.fileinfo[self.index]["pieces root"]
    -            if self.length > self.piece_length:
    -                self.pieces = self.piece_layers[self.root_hash]
    -            else:
    -                self.pieces = self.root_hash
    -            path = self.paths[self.index]
    -            self.prog_start(self.length, path, unit="bytes")
    -            self.count = 0
    -            if os.path.exists(self.current):
    -                self.hasher = FileHasher(path, self.piece_length, progress=0)
    -            else:
    -                self.hasher = self.Padder(self.length, self.piece_length)
    -            return True
    -        if self.index >= len(self.paths):
    -            del self.current
    -            del self.length
    -            del self.root_hash
    -            del self.pieces
    -        return False
    -
    -    def process_current(self) -> tuple:
    -        """
    -        Gather necessary information to compare to metafile details.
    -
    -        Returns
    -        -------
    -        tuple
    -            a tuple containing the layer, piece, current path and size
    -
    -        Raises
    -        ------
    -        StopIteration
    -        """
    -        try:
    -            layer = next(self.hasher)
    -            piece, size = self.advance()
    -            self.prog_update(size)
    -            return layer, piece, self.current, size
    -        except StopIteration as err:
    -            if self.length > 0 and self.count * SHA256 < len(self.pieces):
    -                self.hasher = self.Padder(self.length, self.piece_length)
    -                piece, size = self.advance()
    -                layer = next(self.hasher)
    -                self.prog_update(0)
    -                return layer, piece, self.current, size
    -            raise StopIteration from err
    -
    -    def advance(self) -> tuple:
    -        """
    -        Increment the number of pieces processed for the current file.
    -
    -        Returns
    -        -------
    -        tuple
    -            the piece and size
    -        """
    -        start = self.count * SHA256
    -        end = start + SHA256
    -        piece = self.pieces[start:end]
    -        self.count += 1
    -        if self.length >= self.piece_length:
    -            self.length -= self.piece_length
    -            size = self.piece_length
    -        else:
    -            size = self.length
    -            self.length -= self.length
    -        return piece, size
    -
    -
    -
    @@ -14158,12 +5818,12 @@

    -

    +

    Padder -

    +
    @@ -14205,118 +5865,6 @@
    -
    - Source code in torrentfile\recheck.py -
    518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    -544
    -545
    -546
    -547
    -548
    -549
    -550
    -551
    -552
    -553
    -554
    -555
    -556
    -557
    -558
    -559
    -560
    -561
    -562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    class Padder:
    -    """
    -    Padding class to generate padding hashes wherever needed.
    -
    -    Parameters
    -    ----------
    -    length: int
    -        the total size of the mock file generating padding for.
    -    piece_length : int
    -        the block size that each hash represents.
    -    """
    -
    -    def __init__(self, length, piece_length):
    -        """
    -        Construct padding class to Mock missing or incomplete files.
    -
    -        Parameters
    -        ----------
    -        length : int
    -            size of the file
    -        piece_length : int
    -            the piece length for each iteration.
    -        """
    -        self.length = length
    -        self.piece_length = piece_length
    -        self.pad = sha256(bytearray(piece_length)).digest()
    -
    -    def __iter__(self):
    -        """
    -        Return self to correctly implement iterator type.
    -        """
    -        return self  # pragma: nocover
    -
    -    def __next__(self) -> bytes:
    -        """
    -        Iterate through seemingly endless sha256 hashes of zeros.
    -
    -        Returns
    -        -------
    -        tuple :
    -            returns the padding
    -
    -        Raises
    -        ------
    -        StopIteration
    -        """
    -        if self.length >= self.piece_length:
    -            self.length -= self.piece_length
    -            return self.pad
    -        if self.length > 0:
    -            pad = sha256(bytearray(self.length)).digest()
    -            self.length -= self.length
    -            return pad
    -        raise StopIteration
    -
    -
    -
    @@ -14336,11 +5884,11 @@
    -
    +
    __init__(length, piece_length) -
    +
    @@ -14381,38 +5929,6 @@
    -
    - Source code in torrentfile\recheck.py -
    530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    def __init__(self, length, piece_length):
    -    """
    -    Construct padding class to Mock missing or incomplete files.
    -
    -    Parameters
    -    ----------
    -    length : int
    -        size of the file
    -    piece_length : int
    -        the piece length for each iteration.
    -    """
    -    self.length = length
    -    self.piece_length = piece_length
    -    self.pad = sha256(bytearray(piece_length)).digest()
    -
    -
    -
    @@ -14423,31 +5939,17 @@
    +
    __iter__() -
    +

    Return self to correctly implement iterator type.

    -
    - Source code in torrentfile\recheck.py -
    545
    -546
    -547
    -548
    -549
    def __iter__(self):
    -    """
    -    Return self to correctly implement iterator type.
    -    """
    -    return self  # pragma: nocover
    -
    -
    -
    @@ -14458,11 +5960,11 @@
    +
    __next__() -
    +
    @@ -14505,52 +6007,6 @@
    -
    - Source code in torrentfile\recheck.py -
    551
    -552
    -553
    -554
    -555
    -556
    -557
    -558
    -559
    -560
    -561
    -562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    def __next__(self) -> bytes:
    -    """
    -    Iterate through seemingly endless sha256 hashes of zeros.
    -
    -    Returns
    -    -------
    -    tuple :
    -        returns the padding
    -
    -    Raises
    -    ------
    -    StopIteration
    -    """
    -    if self.length >= self.piece_length:
    -        self.length -= self.piece_length
    -        return self.pad
    -    if self.length > 0:
    -        pad = sha256(bytearray(self.length)).digest()
    -        self.length -= self.length
    -        return pad
    -    raise StopIteration
    -
    -
    -
    @@ -14572,43 +6028,17 @@
    +

    __init__(checker) -

    +

    Construct a HybridChecker instance.

    -
    - Source code in torrentfile\recheck.py -
    487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    def __init__(self, checker: Checker):
    -    """
    -    Construct a HybridChecker instance.
    -    """
    -    self.checker = checker
    -    self.paths = checker.paths
    -    self.piece_length = checker.piece_length
    -    self.fileinfo = checker.fileinfo
    -    self.piece_layers = checker.meta["piece layers"]
    -    self.current = None
    -    self.index = -1
    -
    -
    -
    @@ -14619,31 +6049,17 @@
    -
    +

    __iter__() -

    +

    Assign iterator and return self.

    -
    - Source code in torrentfile\recheck.py -
    499
    -500
    -501
    -502
    -503
    def __iter__(self):
    -    """
    -    Assign iterator and return self.
    -    """
    -    return self
    -
    -
    -
    @@ -14654,45 +6070,17 @@
    -
    +

    __next__() -

    +

    Provide the result of comparison.

    -
    - Source code in torrentfile\recheck.py -
    505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    def __next__(self):
    -    """
    -    Provide the result of comparison.
    -    """
    -    if self.current is None:
    -        self.next_file()
    -    try:
    -        return self.process_current()
    -    except StopIteration as itererr:
    -        if self.next_file():
    -            return self.process_current()
    -        raise StopIteration from itererr
    -
    -
    -
    @@ -14703,11 +6091,11 @@
    -
    +

    advance() -

    +
    @@ -14732,50 +6120,6 @@
    -
    - Source code in torrentfile\recheck.py -
    634
    -635
    -636
    -637
    -638
    -639
    -640
    -641
    -642
    -643
    -644
    -645
    -646
    -647
    -648
    -649
    -650
    -651
    -652
    -653
    def advance(self) -> tuple:
    -    """
    -    Increment the number of pieces processed for the current file.
    -
    -    Returns
    -    -------
    -    tuple
    -        the piece and size
    -    """
    -    start = self.count * SHA256
    -    end = start + SHA256
    -    piece = self.pieces[start:end]
    -    self.count += 1
    -    if self.length >= self.piece_length:
    -        self.length -= self.piece_length
    -        size = self.piece_length
    -    else:
    -        size = self.length
    -        self.length -= self.length
    -    return piece, size
    -
    -
    -
    @@ -14786,11 +6130,11 @@
    -
    +

    next_file() -

    +
    @@ -14815,76 +6159,6 @@
    -
    - Source code in torrentfile\recheck.py -
    573
    -574
    -575
    -576
    -577
    -578
    -579
    -580
    -581
    -582
    -583
    -584
    -585
    -586
    -587
    -588
    -589
    -590
    -591
    -592
    -593
    -594
    -595
    -596
    -597
    -598
    -599
    -600
    -601
    -602
    -603
    -604
    -605
    def next_file(self) -> bool:
    -    """
    -    Remove all references to  processed files and prepare for the next.
    -
    -    Returns
    -    -------
    -    bool
    -        if there is a next file found
    -    """
    -    self.index += 1
    -    self.prog_close()
    -    if self.current is None or self.index < len(self.paths):
    -        self.current = self.paths[self.index]
    -        self.length = self.fileinfo[self.index]["length"]
    -        self.root_hash = self.fileinfo[self.index]["pieces root"]
    -        if self.length > self.piece_length:
    -            self.pieces = self.piece_layers[self.root_hash]
    -        else:
    -            self.pieces = self.root_hash
    -        path = self.paths[self.index]
    -        self.prog_start(self.length, path, unit="bytes")
    -        self.count = 0
    -        if os.path.exists(self.current):
    -            self.hasher = FileHasher(path, self.piece_length, progress=0)
    -        else:
    -            self.hasher = self.Padder(self.length, self.piece_length)
    -        return True
    -    if self.index >= len(self.paths):
    -        del self.current
    -        del self.length
    -        del self.root_hash
    -        del self.pieces
    -    return False
    -
    -
    -
    @@ -14895,11 +6169,11 @@
    -
    +

    process_current() -

    +
    @@ -14942,62 +6216,6 @@
    -
    - Source code in torrentfile\recheck.py -
    607
    -608
    -609
    -610
    -611
    -612
    -613
    -614
    -615
    -616
    -617
    -618
    -619
    -620
    -621
    -622
    -623
    -624
    -625
    -626
    -627
    -628
    -629
    -630
    -631
    -632
    def process_current(self) -> tuple:
    -    """
    -    Gather necessary information to compare to metafile details.
    -
    -    Returns
    -    -------
    -    tuple
    -        a tuple containing the layer, piece, current path and size
    -
    -    Raises
    -    ------
    -    StopIteration
    -    """
    -    try:
    -        layer = next(self.hasher)
    -        piece, size = self.advance()
    -        self.prog_update(size)
    -        return layer, piece, self.current, size
    -    except StopIteration as err:
    -        if self.length > 0 and self.count * SHA256 < len(self.pieces):
    -            self.hasher = self.Padder(self.length, self.piece_length)
    -            piece, size = self.advance()
    -            layer = next(self.hasher)
    -            self.prog_update(0)
    -            return layer, piece, self.current, size
    -        raise StopIteration from err
    -
    -
    -
    @@ -15029,17 +6247,17 @@
    +

    torrent -

    +

    Classes and procedures pertaining to the creation of torrent meta files.

    -
    Classes
    +

    Classes

    • TorrentFile @@ -15054,7 +6272,7 @@

      Classes
      base class for all MetaFile classes.

    -
    Constants
    +

    Constants

    • BLOCK_SIZE : int @@ -15065,14 +6283,14 @@

      Constants
      Length of a sha256 hash.

    -
    Bittorrent V2
    +

    Bittorrent V2

    From Bittorrent.org Documentation pages.

    Implementation details for Bittorrent Protocol v2.

    Note

    All strings in a .torrent file that contain text must be UTF-8 encoded.

    -
    Meta Version 2 Dictionary:
    +
    Meta Version 2 Dictionary:
    • "announce": @@ -15165,8 +6383,8 @@

      Meta Version 2 Dictionar i.e. it must not contain a zero-length key with a dictionary containing a length key.

    -
    Bittorrent V1
    -
    v1 meta-dictionary
    +

    Bittorrent V1

    +
    v1 meta-dictionary
    • announce: @@ -15251,12 +6469,12 @@

      v1 meta-dictionary
      -

      +

      MetaFile -

      +
      @@ -15409,408 +6627,6 @@

    -
    - Source code in torrentfile\torrent.py -
    204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    class MetaFile:
    -    """
    -    Base Class for all TorrentFile classes.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        target path to torrent content.  Default: None
    -    announce : str
    -        One or more tracker URL's.  Default: None
    -    comment : str
    -        A comment.  Default: None
    -    piece_length : int
    -        Size of torrent pieces.  Default: None
    -    private : bool
    -        For private trackers.  Default: None
    -    outfile : str
    -        target path to write .torrent file. Default: None
    -    source : str
    -        Private tracker source. Default: None
    -    progress : str
    -        level of progress bar displayed  Default: "1"
    -    cwd : bool
    -        If True change default save location to current directory
    -    httpseeds : list
    -        one or more web addresses where torrent content can be found.
    -    url_list : list
    -        one or more web addressess where torrent content exists.
    -    content : str
    -        alias for 'path' arg.
    -    meta_version : int
    -        indicates which Bittorrent protocol to use for hashing content
    -    """
    -
    -    hasher = None
    -
    -    @classmethod
    -    def set_callback(cls, func):
    -        """
    -        Assign a callback function for the Hashing class to call for each hash.
    -
    -        Parameters
    -        ----------
    -        func : function
    -            The callback function which accepts a single paramter.
    -        """
    -        if "hasher" in vars(cls) and vars(cls)["hasher"]:
    -            cls.hasher.set_callback(func)
    -
    -    def __init__(
    -        self,
    -        path=None,
    -        announce=None,
    -        comment=None,
    -        piece_length=None,
    -        private=False,
    -        outfile=None,
    -        source=None,
    -        progress=1,
    -        cwd=False,
    -        httpseeds=None,
    -        url_list=None,
    -        content=None,
    -        meta_version=None,
    -        **_,
    -    ):
    -        """
    -        Construct MetaFile superclass and assign local attributes.
    -        """
    -        self.private = private
    -        self.cwd = cwd
    -        self.outfile = outfile
    -        self.progress = int(progress)
    -        self.comment = comment
    -        self.source = source
    -        self.meta_version = meta_version
    -
    -        if content:
    -            path = content
    -        if not path:
    -            if announce and len(announce) > 1 and os.path.exists(announce[-1]):
    -                path = announce[-1]
    -                announce = announce[:-1]
    -            elif url_list and os.path.exists(url_list[-1]):
    -                path = url_list[-1]
    -                url_list = url_list[:-1]
    -            elif httpseeds and os.path.exists(httpseeds[-1]):
    -                path = httpseeds[-1]
    -                httpseeds = httpseeds[:-1]
    -            else:
    -                raise utils.MissingPathError("Path to content is required.")
    -
    -        # base path to torrent content.
    -        self.path = path
    -
    -        logger.debug("path parameter found %s", path)
    -
    -        # Format piece_length attribute.
    -        if piece_length:
    -            self.piece_length = utils.normalize_piece_length(piece_length)
    -            logger.debug("piece length parameter found %s", piece_length)
    -        else:
    -            self.piece_length = utils.path_piece_length(self.path)
    -            logger.debug("piece length calculated %s", self.piece_length)
    -
    -        # Assign announce URL to empty string if none provided.
    -        if not announce:
    -            self.announce, self.announce_list = "", [[""]]
    -
    -        # Most torrent clients have editting trackers as a feature.
    -        elif isinstance(announce, str):
    -            self.announce, self.announce_list = announce, [[announce]]
    -
    -        elif isinstance(announce, Sequence):
    -            self.announce, self.announce_list = announce[0], [announce]
    -
    -        self.meta = {
    -            "announce": self.announce,
    -            "announce-list": self.announce_list,
    -            "created by": f"TorrentFile:v{version}",
    -            "creation date": int(datetime.timestamp(datetime.now())),
    -            "info": {},
    -        }
    -        if comment:
    -            self.meta["info"]["comment"] = comment
    -            logger.debug("comment parameter found %s", comment)
    -        if private:
    -            self.meta["info"]["private"] = 1
    -            logger.debug("private parameter triggered")
    -        if source:
    -            self.meta["info"]["source"] = source
    -            logger.debug("source parameter found %s", source)
    -        if url_list:
    -            self.meta["url-list"] = url_list
    -            logger.debug("url list parameter found %s", str(url_list))
    -        if httpseeds:
    -            self.meta["httpseeds"] = httpseeds
    -            logger.debug("httpseeds parameter found %s", str(httpseeds))
    -        self.meta["info"]["piece length"] = self.piece_length
    -
    -        self.meta_version = meta_version
    -        parent, self.name = os.path.split(self.path)
    -        if not self.name:
    -            self.name = os.path.basename(parent)
    -        self.meta["info"]["name"] = self.name
    -
    -    def assemble(self):
    -        """
    -        Overload in subclasses.
    -
    -        Raises
    -        ------
    -        Exception
    -            NotImplementedError
    -        """
    -        raise NotImplementedError
    -
    -    def sort_meta(self):
    -        """Sort the info and meta dictionaries."""
    -        logger.debug("sorting dictionary keys")
    -        meta = self.meta
    -        meta["info"] = dict(sorted(list(meta["info"].items())))
    -        meta = dict(sorted(list(meta.items())))
    -        return meta
    -
    -    def write(self, outfile=None) -> tuple:
    -        """
    -        Write meta information to .torrent file.
    -
    -        Parameters
    -        ----------
    -        outfile : str
    -            Destination path for .torrent file. default=None
    -
    -        Returns
    -        -------
    -        outfile : str
    -            Where the .torrent file was writen.
    -        meta : dict
    -            .torrent meta information.
    -        """
    -        fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
    -        if not self.outfile and not outfile:
    -            if self.cwd:
    -                self.outfile = fallback
    -            else:
    -                path = str(self.path).rstrip("\\/")
    -                self.outfile = path + ".torrent"
    -        elif outfile:
    -            self.outfile = outfile
    -        if str(self.outfile)[-1] in "\\/":
    -            self.outfile = self.outfile + (self.name + ".torrent")
    -        self.meta = self.sort_meta()
    -        try:
    -            pyben.dump(self.meta, self.outfile)
    -        except PermissionError:
    -            self.outfile = fallback
    -            pyben.dump(self.meta, fallback)
    -        return self.outfile, self.meta
    -
    -
    -
    @@ -15839,213 +6655,17 @@

    -

    +

    __init__(path=None, announce=None, comment=None, piece_length=None, private=False, outfile=None, source=None, progress=1, cwd=False, httpseeds=None, url_list=None, content=None, meta_version=None, **_) -

    +

    Construct MetaFile superclass and assign local attributes.

    -
    - Source code in torrentfile\torrent.py -
    253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    def __init__(
    -    self,
    -    path=None,
    -    announce=None,
    -    comment=None,
    -    piece_length=None,
    -    private=False,
    -    outfile=None,
    -    source=None,
    -    progress=1,
    -    cwd=False,
    -    httpseeds=None,
    -    url_list=None,
    -    content=None,
    -    meta_version=None,
    -    **_,
    -):
    -    """
    -    Construct MetaFile superclass and assign local attributes.
    -    """
    -    self.private = private
    -    self.cwd = cwd
    -    self.outfile = outfile
    -    self.progress = int(progress)
    -    self.comment = comment
    -    self.source = source
    -    self.meta_version = meta_version
    -
    -    if content:
    -        path = content
    -    if not path:
    -        if announce and len(announce) > 1 and os.path.exists(announce[-1]):
    -            path = announce[-1]
    -            announce = announce[:-1]
    -        elif url_list and os.path.exists(url_list[-1]):
    -            path = url_list[-1]
    -            url_list = url_list[:-1]
    -        elif httpseeds and os.path.exists(httpseeds[-1]):
    -            path = httpseeds[-1]
    -            httpseeds = httpseeds[:-1]
    -        else:
    -            raise utils.MissingPathError("Path to content is required.")
    -
    -    # base path to torrent content.
    -    self.path = path
    -
    -    logger.debug("path parameter found %s", path)
    -
    -    # Format piece_length attribute.
    -    if piece_length:
    -        self.piece_length = utils.normalize_piece_length(piece_length)
    -        logger.debug("piece length parameter found %s", piece_length)
    -    else:
    -        self.piece_length = utils.path_piece_length(self.path)
    -        logger.debug("piece length calculated %s", self.piece_length)
    -
    -    # Assign announce URL to empty string if none provided.
    -    if not announce:
    -        self.announce, self.announce_list = "", [[""]]
    -
    -    # Most torrent clients have editting trackers as a feature.
    -    elif isinstance(announce, str):
    -        self.announce, self.announce_list = announce, [[announce]]
    -
    -    elif isinstance(announce, Sequence):
    -        self.announce, self.announce_list = announce[0], [announce]
    -
    -    self.meta = {
    -        "announce": self.announce,
    -        "announce-list": self.announce_list,
    -        "created by": f"TorrentFile:v{version}",
    -        "creation date": int(datetime.timestamp(datetime.now())),
    -        "info": {},
    -    }
    -    if comment:
    -        self.meta["info"]["comment"] = comment
    -        logger.debug("comment parameter found %s", comment)
    -    if private:
    -        self.meta["info"]["private"] = 1
    -        logger.debug("private parameter triggered")
    -    if source:
    -        self.meta["info"]["source"] = source
    -        logger.debug("source parameter found %s", source)
    -    if url_list:
    -        self.meta["url-list"] = url_list
    -        logger.debug("url list parameter found %s", str(url_list))
    -    if httpseeds:
    -        self.meta["httpseeds"] = httpseeds
    -        logger.debug("httpseeds parameter found %s", str(httpseeds))
    -    self.meta["info"]["piece length"] = self.piece_length
    -
    -    self.meta_version = meta_version
    -    parent, self.name = os.path.split(self.path)
    -    if not self.name:
    -        self.name = os.path.basename(parent)
    -    self.meta["info"]["name"] = self.name
    -
    -
    -
    @@ -16056,11 +6676,11 @@
    -
    +

    assemble() -

    +
    @@ -16085,30 +6705,6 @@
    -
    - Source code in torrentfile\torrent.py -
    350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    def assemble(self):
    -    """
    -    Overload in subclasses.
    -
    -    Raises
    -    ------
    -    Exception
    -        NotImplementedError
    -    """
    -    raise NotImplementedError
    -
    -
    -
    @@ -16119,14 +6715,14 @@
    -
    +

    set_callback(func) classmethod -

    +
    @@ -16157,34 +6753,6 @@
    -
    - Source code in torrentfile\torrent.py -
    240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    @classmethod
    -def set_callback(cls, func):
    -    """
    -    Assign a callback function for the Hashing class to call for each hash.
    -
    -    Parameters
    -    ----------
    -    func : function
    -        The callback function which accepts a single paramter.
    -    """
    -    if "hasher" in vars(cls) and vars(cls)["hasher"]:
    -        cls.hasher.set_callback(func)
    -
    -
    -
    @@ -16195,35 +6763,17 @@
    -
    +

    sort_meta() -

    +

    Sort the info and meta dictionaries.

    -
    - Source code in torrentfile\torrent.py -
    361
    -362
    -363
    -364
    -365
    -366
    -367
    def sort_meta(self):
    -    """Sort the info and meta dictionaries."""
    -    logger.debug("sorting dictionary keys")
    -    meta = self.meta
    -    meta["info"] = dict(sorted(list(meta["info"].items())))
    -    meta = dict(sorted(list(meta.items())))
    -    return meta
    -
    -
    -
    @@ -16234,11 +6784,11 @@
    -
    +

    write(outfile=None) -

    +
    @@ -16293,78 +6843,6 @@
    -
    - Source code in torrentfile\torrent.py -
    369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    def write(self, outfile=None) -> tuple:
    -    """
    -    Write meta information to .torrent file.
    -
    -    Parameters
    -    ----------
    -    outfile : str
    -        Destination path for .torrent file. default=None
    -
    -    Returns
    -    -------
    -    outfile : str
    -        Where the .torrent file was writen.
    -    meta : dict
    -        .torrent meta information.
    -    """
    -    fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
    -    if not self.outfile and not outfile:
    -        if self.cwd:
    -            self.outfile = fallback
    -        else:
    -            path = str(self.path).rstrip("\\/")
    -            self.outfile = path + ".torrent"
    -    elif outfile:
    -        self.outfile = outfile
    -    if str(self.outfile)[-1] in "\\/":
    -        self.outfile = self.outfile + (self.name + ".torrent")
    -    self.meta = self.sort_meta()
    -    try:
    -        pyben.dump(self.meta, self.outfile)
    -    except PermissionError:
    -        self.outfile = fallback
    -        pyben.dump(self.meta, fallback)
    -    return self.outfile, self.meta
    -
    -
    -
    @@ -16385,12 +6863,12 @@
    -

    +

    TorrentAssembler -

    +
    @@ -16427,204 +6905,6 @@

    -
    - Source code in torrentfile\torrent.py -
    643
    -644
    -645
    -646
    -647
    -648
    -649
    -650
    -651
    -652
    -653
    -654
    -655
    -656
    -657
    -658
    -659
    -660
    -661
    -662
    -663
    -664
    -665
    -666
    -667
    -668
    -669
    -670
    -671
    -672
    -673
    -674
    -675
    -676
    -677
    -678
    -679
    -680
    -681
    -682
    -683
    -684
    -685
    -686
    -687
    -688
    -689
    -690
    -691
    -692
    -693
    -694
    -695
    -696
    -697
    -698
    -699
    -700
    -701
    -702
    -703
    -704
    -705
    -706
    -707
    -708
    -709
    -710
    -711
    -712
    -713
    -714
    -715
    -716
    -717
    -718
    -719
    -720
    -721
    -722
    -723
    -724
    -725
    -726
    -727
    -728
    -729
    -730
    -731
    -732
    -733
    -734
    -735
    -736
    -737
    -738
    -739
    class TorrentAssembler(MetaFile):
    -    """
    -    Assembler class for Bittorrent version 2 and hybrid meta files.
    -
    -    This differs from the TorrentFileV2 and TorrentFileHybrid, because
    -    it can be used as an iterator and works for both versions.
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        Keyword arguments for torrent options.
    -    """
    -
    -    hasher = FileHasher
    -
    -    def __init__(self, **kwargs):
    -        """
    -        Create Bittorrent v1 v2 hybrid metafiles.
    -        """
    -        super().__init__(**kwargs)
    -        logger.debug("Assembling bittorrent Hybrid file")
    -        self.name = os.path.basename(self.path)
    -        self.hashes = []
    -        self.piece_layers = {}
    -        self.pieces = bytearray()
    -        self.files = []
    -        self.hybrid = self.meta_version == "3"
    -        self.total = len(utils.get_file_list(self.path))
    -        self.assemble()
    -
    -    def assemble(self):
    -        """
    -        Assemble the parts of the torrentfile into meta dictionary.
    -        """
    -        info = self.meta["info"]
    -        info["meta version"] = 2
    -
    -        if os.path.isfile(self.path):
    -            info["file tree"] = {self.name: self._traverse(self.path)}
    -            info["length"] = os.path.getsize(self.path)
    -
    -        else:
    -            info["file tree"] = self._traverse(self.path)
    -            if self.hybrid:
    -                info["files"] = self.files
    -
    -        if self.hybrid:
    -            info["pieces"] = self.pieces
    -        self.meta["piece layers"] = self.piece_layers
    -        return info
    -
    -    def _traverse(self, path: str) -> dict:
    -        """
    -        Build meta dictionary while walking directory.
    -
    -        Parameters
    -        ----------
    -        path : str
    -            Path to target file.
    -        """
    -        if os.path.isfile(path):
    -            file_size = os.path.getsize(path)
    -            if self.hybrid:
    -                self.files.append(
    -                    {
    -                        "length": file_size,
    -                        "path": os.path.relpath(path, self.path).split(os.sep),
    -                    }
    -                )
    -
    -            if file_size == 0:
    -                return {"": {"length": file_size}}
    -
    -            logger.debug("Hashing %s", str(path))
    -            hasher = FileHasher(
    -                path, self.piece_length, progress=True, hybrid=self.hybrid
    -            )
    -            layers = bytearray()
    -            for result in hasher:
    -                if self.hybrid:
    -                    layer_hash, piece = result
    -                    self.pieces.extend(piece)
    -                else:
    -                    layer_hash = result
    -                layers.extend(layer_hash)
    -            if file_size > self.piece_length:
    -                self.piece_layers[hasher.root] = layers
    -            if self.hybrid and hasher.padding_file:
    -                self.files.append(hasher.padding_file)
    -
    -            return {"": {"length": file_size, "pieces root": hasher.root}}
    -
    -        tree = {}
    -        if os.path.isdir(path):
    -            for name in sorted(os.listdir(path)):
    -                tree[name] = self._traverse(os.path.join(path, name))
    -        return tree
    -
    -
    -
    @@ -16649,49 +6929,17 @@

    -

    +

    __init__(**kwargs) -

    +

    Create Bittorrent v1 v2 hybrid metafiles.

    -
    - Source code in torrentfile\torrent.py -
    658
    -659
    -660
    -661
    -662
    -663
    -664
    -665
    -666
    -667
    -668
    -669
    -670
    -671
    def __init__(self, **kwargs):
    -    """
    -    Create Bittorrent v1 v2 hybrid metafiles.
    -    """
    -    super().__init__(**kwargs)
    -    logger.debug("Assembling bittorrent Hybrid file")
    -    self.name = os.path.basename(self.path)
    -    self.hashes = []
    -    self.piece_layers = {}
    -    self.pieces = bytearray()
    -    self.files = []
    -    self.hybrid = self.meta_version == "3"
    -    self.total = len(utils.get_file_list(self.path))
    -    self.assemble()
    -
    -
    -
    @@ -16702,11 +6950,11 @@
    -
    +

    _traverse(path) -

    +
    @@ -16737,102 +6985,6 @@
    -
    - Source code in torrentfile\torrent.py -
    694
    -695
    -696
    -697
    -698
    -699
    -700
    -701
    -702
    -703
    -704
    -705
    -706
    -707
    -708
    -709
    -710
    -711
    -712
    -713
    -714
    -715
    -716
    -717
    -718
    -719
    -720
    -721
    -722
    -723
    -724
    -725
    -726
    -727
    -728
    -729
    -730
    -731
    -732
    -733
    -734
    -735
    -736
    -737
    -738
    -739
    def _traverse(self, path: str) -> dict:
    -    """
    -    Build meta dictionary while walking directory.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to target file.
    -    """
    -    if os.path.isfile(path):
    -        file_size = os.path.getsize(path)
    -        if self.hybrid:
    -            self.files.append(
    -                {
    -                    "length": file_size,
    -                    "path": os.path.relpath(path, self.path).split(os.sep),
    -                }
    -            )
    -
    -        if file_size == 0:
    -            return {"": {"length": file_size}}
    -
    -        logger.debug("Hashing %s", str(path))
    -        hasher = FileHasher(
    -            path, self.piece_length, progress=True, hybrid=self.hybrid
    -        )
    -        layers = bytearray()
    -        for result in hasher:
    -            if self.hybrid:
    -                layer_hash, piece = result
    -                self.pieces.extend(piece)
    -            else:
    -                layer_hash = result
    -            layers.extend(layer_hash)
    -        if file_size > self.piece_length:
    -            self.piece_layers[hasher.root] = layers
    -        if self.hybrid and hasher.padding_file:
    -            self.files.append(hasher.padding_file)
    -
    -        return {"": {"length": file_size, "pieces root": hasher.root}}
    -
    -    tree = {}
    -    if os.path.isdir(path):
    -        for name in sorted(os.listdir(path)):
    -            tree[name] = self._traverse(os.path.join(path, name))
    -    return tree
    -
    -
    -
    @@ -16843,61 +6995,17 @@
    -
    +

    assemble() -

    +

    Assemble the parts of the torrentfile into meta dictionary.

    -
    - Source code in torrentfile\torrent.py -
    673
    -674
    -675
    -676
    -677
    -678
    -679
    -680
    -681
    -682
    -683
    -684
    -685
    -686
    -687
    -688
    -689
    -690
    -691
    -692
    def assemble(self):
    -    """
    -    Assemble the parts of the torrentfile into meta dictionary.
    -    """
    -    info = self.meta["info"]
    -    info["meta version"] = 2
    -
    -    if os.path.isfile(self.path):
    -        info["file tree"] = {self.name: self._traverse(self.path)}
    -        info["length"] = os.path.getsize(self.path)
    -
    -    else:
    -        info["file tree"] = self._traverse(self.path)
    -        if self.hybrid:
    -            info["files"] = self.files
    -
    -    if self.hybrid:
    -        info["pieces"] = self.pieces
    -    self.meta["piece layers"] = self.piece_layers
    -    return info
    -
    -
    -
    @@ -16918,12 +7026,12 @@
    -

    +

    TorrentFile -

    +
    @@ -16959,120 +7067,6 @@

    -
    - Source code in torrentfile\torrent.py -
    405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    class TorrentFile(MetaFile, ProgMixin):
    -    """
    -    Class for creating Bittorrent meta files.
    -
    -    Construct *Torrentfile* class instance object.
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        Dictionary containing torrent file options.
    -    """
    -
    -    hasher = Hasher
    -
    -    def __init__(self, **kwargs):
    -        """
    -        Construct TorrentFile instance with given keyword args.
    -
    -        Parameters
    -        ----------
    -        **kwargs : dict
    -            dictionary of keyword args passed to superclass.
    -        """
    -        super().__init__(**kwargs)
    -        logger.debug("Assembling bittorrent v1 torrent file")
    -        self.assemble()
    -
    -    def assemble(self):
    -        """
    -        Assemble components of torrent metafile.
    -
    -        Returns
    -        -------
    -        dict
    -            metadata dictionary for torrent file
    -        """
    -        info = self.meta["info"]
    -        size, filelist = utils.filelist_total(self.path)
    -        if os.path.isfile(self.path):
    -            info["length"] = size
    -        else:
    -            info["files"] = [
    -                {
    -                    "length": os.path.getsize(path),
    -                    "path": os.path.relpath(path, self.path).split(os.sep),
    -                }
    -                for path in filelist
    -            ]
    -        pieces = bytearray()
    -
    -        feeder = Hasher(filelist, self.piece_length, self.progress)
    -        for piece in feeder:
    -            pieces.extend(piece)
    -
    -        info["pieces"] = pieces
    -
    -
    -
    @@ -17090,11 +7084,11 @@

    -

    +

    __init__(**kwargs) -

    +
    @@ -17125,34 +7119,6 @@
    -
    - Source code in torrentfile\torrent.py -
    419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    def __init__(self, **kwargs):
    -    """
    -    Construct TorrentFile instance with given keyword args.
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        dictionary of keyword args passed to superclass.
    -    """
    -    super().__init__(**kwargs)
    -    logger.debug("Assembling bittorrent v1 torrent file")
    -    self.assemble()
    -
    -
    -
    @@ -17163,11 +7129,11 @@
    -
    +

    assemble() -

    +
    @@ -17192,66 +7158,6 @@
    -
    - Source code in torrentfile\torrent.py -
    432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    def assemble(self):
    -    """
    -    Assemble components of torrent metafile.
    -
    -    Returns
    -    -------
    -    dict
    -        metadata dictionary for torrent file
    -    """
    -    info = self.meta["info"]
    -    size, filelist = utils.filelist_total(self.path)
    -    if os.path.isfile(self.path):
    -        info["length"] = size
    -    else:
    -        info["files"] = [
    -            {
    -                "length": os.path.getsize(path),
    -                "path": os.path.relpath(path, self.path).split(os.sep),
    -            }
    -            for path in filelist
    -        ]
    -    pieces = bytearray()
    -
    -    feeder = Hasher(filelist, self.piece_length, self.progress)
    -    for piece in feeder:
    -        pieces.extend(piece)
    -
    -    info["pieces"] = pieces
    -
    -
    -
    @@ -17272,12 +7178,12 @@
    -

    +

    TorrentFileHybrid -

    +
    @@ -17313,196 +7219,6 @@

    -
    - Source code in torrentfile\torrent.py -
    548
    -549
    -550
    -551
    -552
    -553
    -554
    -555
    -556
    -557
    -558
    -559
    -560
    -561
    -562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    -572
    -573
    -574
    -575
    -576
    -577
    -578
    -579
    -580
    -581
    -582
    -583
    -584
    -585
    -586
    -587
    -588
    -589
    -590
    -591
    -592
    -593
    -594
    -595
    -596
    -597
    -598
    -599
    -600
    -601
    -602
    -603
    -604
    -605
    -606
    -607
    -608
    -609
    -610
    -611
    -612
    -613
    -614
    -615
    -616
    -617
    -618
    -619
    -620
    -621
    -622
    -623
    -624
    -625
    -626
    -627
    -628
    -629
    -630
    -631
    -632
    -633
    -634
    -635
    -636
    -637
    -638
    -639
    -640
    class TorrentFileHybrid(MetaFile, ProgMixin):
    -    """
    -    Construct the Hybrid torrent meta file with provided parameters.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        Keyword arguments for torrent options.
    -    """
    -
    -    hasher = HasherHybrid
    -
    -    def __init__(self, **kwargs):
    -        """
    -        Create Bittorrent v1 v2 hybrid metafiles.
    -        """
    -        super().__init__(**kwargs)
    -        logger.debug("Assembling bittorrent Hybrid file")
    -        self.name = os.path.basename(self.path)
    -        self.hashes = []
    -        self.piece_layers = {}
    -        self.pieces = []
    -        self.files = []
    -        self.total = len(utils.get_file_list(self.path))
    -        self.assemble()
    -
    -    def assemble(self):
    -        """
    -        Assemble the parts of the torrentfile into meta dictionary.
    -
    -        **DEPRECATED**
    -        """
    -        info = self.meta["info"]
    -        info["meta version"] = 2
    -
    -        if os.path.isfile(self.path):
    -            info["file tree"] = {self.name: self._traverse(self.path)}
    -            info["length"] = os.path.getsize(self.path)
    -
    -        else:
    -            info["file tree"] = self._traverse(self.path)
    -            info["files"] = self.files
    -
    -        info["pieces"] = b"".join(self.pieces)
    -        self.meta["piece layers"] = self.piece_layers
    -        return info
    -
    -    def _traverse(self, path: str) -> dict:
    -        """
    -        Build meta dictionary while walking directory.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        path : str
    -            Path to target file.
    -        """
    -        if os.path.isfile(path):
    -            file_size = os.path.getsize(path)
    -
    -            self.files.append(
    -                {
    -                    "length": file_size,
    -                    "path": os.path.relpath(path, self.path).split(os.sep),
    -                }
    -            )
    -
    -            if file_size == 0:
    -                return {"": {"length": file_size}}
    -
    -            logger.debug("Hashing %s", str(path))
    -            file_hash = HasherHybrid(path, self.piece_length, self.progress)
    -            self.prog_update(file_size)
    -
    -            if file_size > self.piece_length:
    -                self.piece_layers[file_hash.root] = file_hash.piece_layer
    -
    -            self.hashes.append(file_hash)
    -            self.pieces.extend(file_hash.pieces)
    -
    -            if file_hash.padding_file:
    -                self.files.append(file_hash.padding_file)
    -
    -            return {"": {"length": file_size, "pieces root": file_hash.root}}
    -
    -        tree = {}
    -        if os.path.isdir(path):
    -            for name in sorted(os.listdir(path)):
    -                tree[name] = self._traverse(os.path.join(path, name))
    -        return tree
    -
    -
    -
    @@ -17526,47 +7242,17 @@

    -

    +

    __init__(**kwargs) -

    +

    Create Bittorrent v1 v2 hybrid metafiles.

    -
    - Source code in torrentfile\torrent.py -
    562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    -572
    -573
    -574
    def __init__(self, **kwargs):
    -    """
    -    Create Bittorrent v1 v2 hybrid metafiles.
    -    """
    -    super().__init__(**kwargs)
    -    logger.debug("Assembling bittorrent Hybrid file")
    -    self.name = os.path.basename(self.path)
    -    self.hashes = []
    -    self.piece_layers = {}
    -    self.pieces = []
    -    self.files = []
    -    self.total = len(utils.get_file_list(self.path))
    -    self.assemble()
    -
    -
    -
    @@ -17577,11 +7263,11 @@
    -
    +

    _traverse(path) -

    +
    @@ -17613,98 +7299,6 @@
    -
    - Source code in torrentfile\torrent.py -
    597
    -598
    -599
    -600
    -601
    -602
    -603
    -604
    -605
    -606
    -607
    -608
    -609
    -610
    -611
    -612
    -613
    -614
    -615
    -616
    -617
    -618
    -619
    -620
    -621
    -622
    -623
    -624
    -625
    -626
    -627
    -628
    -629
    -630
    -631
    -632
    -633
    -634
    -635
    -636
    -637
    -638
    -639
    -640
    def _traverse(self, path: str) -> dict:
    -    """
    -    Build meta dictionary while walking directory.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to target file.
    -    """
    -    if os.path.isfile(path):
    -        file_size = os.path.getsize(path)
    -
    -        self.files.append(
    -            {
    -                "length": file_size,
    -                "path": os.path.relpath(path, self.path).split(os.sep),
    -            }
    -        )
    -
    -        if file_size == 0:
    -            return {"": {"length": file_size}}
    -
    -        logger.debug("Hashing %s", str(path))
    -        file_hash = HasherHybrid(path, self.piece_length, self.progress)
    -        self.prog_update(file_size)
    -
    -        if file_size > self.piece_length:
    -            self.piece_layers[file_hash.root] = file_hash.piece_layer
    -
    -        self.hashes.append(file_hash)
    -        self.pieces.extend(file_hash.pieces)
    -
    -        if file_hash.padding_file:
    -            self.files.append(file_hash.padding_file)
    -
    -        return {"": {"length": file_size, "pieces root": file_hash.root}}
    -
    -    tree = {}
    -    if os.path.isdir(path):
    -        for name in sorted(os.listdir(path)):
    -            tree[name] = self._traverse(os.path.join(path, name))
    -    return tree
    -
    -
    -
    @@ -17715,11 +7309,11 @@
    +

    assemble() -

    +
    @@ -17727,50 +7321,6 @@

    Assemble the parts of the torrentfile into meta dictionary.

    DEPRECATED

    -
    - Source code in torrentfile\torrent.py -
    576
    -577
    -578
    -579
    -580
    -581
    -582
    -583
    -584
    -585
    -586
    -587
    -588
    -589
    -590
    -591
    -592
    -593
    -594
    -595
    def assemble(self):
    -    """
    -    Assemble the parts of the torrentfile into meta dictionary.
    -
    -    **DEPRECATED**
    -    """
    -    info = self.meta["info"]
    -    info["meta version"] = 2
    -
    -    if os.path.isfile(self.path):
    -        info["file tree"] = {self.name: self._traverse(self.path)}
    -        info["length"] = os.path.getsize(self.path)
    -
    -    else:
    -        info["file tree"] = self._traverse(self.path)
    -        info["files"] = self.files
    -
    -    info["pieces"] = b"".join(self.pieces)
    -    self.meta["piece layers"] = self.piece_layers
    -    return info
    -
    -
    -
    @@ -17791,12 +7341,12 @@
    -

    +

    TorrentFileV2 -

    +
    @@ -17832,178 +7382,6 @@

    -
    - Source code in torrentfile\torrent.py -
    462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    -544
    -545
    class TorrentFileV2(MetaFile, ProgMixin):
    -    """
    -    Class for creating Bittorrent meta v2 files.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        Keyword arguments for torrent file options.
    -    """
    -
    -    hasher = HasherV2
    -
    -    def __init__(self, **kwargs):
    -        """
    -        Construct `TorrentFileV2` Class instance from given parameters.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        **kwargs : dict
    -            keywword arguments to pass to superclass.
    -        """
    -        super().__init__(**kwargs)
    -        logger.debug("Assembling bittorrent v2 torrent file")
    -        self.piece_layers = {}
    -        self.hashes = []
    -        self.total = len(utils.get_file_list(self.path))
    -        self.assemble()
    -
    -    def assemble(self):
    -        """
    -        Assemble then return the meta dictionary for encoding.
    -
    -        **DEPRECATED**
    -
    -        Returns
    -        -------
    -        meta : dict
    -            Metainformation about the torrent.
    -        """
    -        info = self.meta["info"]
    -        if os.path.isfile(self.path):
    -            info["file tree"] = {info["name"]: self._traverse(self.path)}
    -            info["length"] = os.path.getsize(self.path)
    -            self.prog_update(info["length"])
    -        else:
    -            info["file tree"] = self._traverse(self.path)
    -
    -        info["meta version"] = 2
    -        self.meta["piece layers"] = self.piece_layers
    -
    -    def _traverse(self, path: str) -> dict:
    -        """
    -        Walk directory tree.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        path : str
    -            Path to file or directory.
    -        """
    -        if os.path.isfile(path):
    -            # Calculate Size and hashes for each file.
    -            size = os.path.getsize(path)
    -
    -            if size == 0:
    -                return {"": {"length": size}}
    -
    -            logger.debug("Hashing %s", str(path))
    -            fhash = HasherV2(path, self.piece_length, self.progress)
    -
    -            if size > self.piece_length:
    -                self.piece_layers[fhash.root] = fhash.piece_layer
    -            return {"": {"length": size, "pieces root": fhash.root}}
    -
    -        file_tree = {}
    -        if os.path.isdir(path):
    -            for name in sorted(os.listdir(path)):
    -                file_tree[name] = self._traverse(os.path.join(path, name))
    -        return file_tree
    -
    -
    -
    @@ -18024,11 +7402,11 @@

    -

    +

    __init__(**kwargs) -

    +
    @@ -18060,44 +7438,6 @@
    -
    - Source code in torrentfile\torrent.py -
    476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    def __init__(self, **kwargs):
    -    """
    -    Construct `TorrentFileV2` Class instance from given parameters.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        keywword arguments to pass to superclass.
    -    """
    -    super().__init__(**kwargs)
    -    logger.debug("Assembling bittorrent v2 torrent file")
    -    self.piece_layers = {}
    -    self.hashes = []
    -    self.total = len(utils.get_file_list(self.path))
    -    self.assemble()
    -
    -
    -
    @@ -18108,11 +7448,11 @@
    -
    +

    _traverse(path) -

    +
    @@ -18144,70 +7484,6 @@
    -
    - Source code in torrentfile\torrent.py -
    516
    -517
    -518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    -544
    -545
    def _traverse(self, path: str) -> dict:
    -    """
    -    Walk directory tree.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to file or directory.
    -    """
    -    if os.path.isfile(path):
    -        # Calculate Size and hashes for each file.
    -        size = os.path.getsize(path)
    -
    -        if size == 0:
    -            return {"": {"length": size}}
    -
    -        logger.debug("Hashing %s", str(path))
    -        fhash = HasherV2(path, self.piece_length, self.progress)
    -
    -        if size > self.piece_length:
    -            self.piece_layers[fhash.root] = fhash.piece_layer
    -        return {"": {"length": size, "pieces root": fhash.root}}
    -
    -    file_tree = {}
    -    if os.path.isdir(path):
    -        for name in sorted(os.listdir(path)):
    -            file_tree[name] = self._traverse(os.path.join(path, name))
    -    return file_tree
    -
    -
    -
    @@ -18218,11 +7494,11 @@
    -
    +

    assemble() -

    +
    @@ -18248,52 +7524,6 @@
    -
    - Source code in torrentfile\torrent.py -
    494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    def assemble(self):
    -    """
    -    Assemble then return the meta dictionary for encoding.
    -
    -    **DEPRECATED**
    -
    -    Returns
    -    -------
    -    meta : dict
    -        Metainformation about the torrent.
    -    """
    -    info = self.meta["info"]
    -    if os.path.isfile(self.path):
    -        info["file tree"] = {info["name"]: self._traverse(self.path)}
    -        info["length"] = os.path.getsize(self.path)
    -        self.prog_update(info["length"])
    -    else:
    -        info["file tree"] = self._traverse(self.path)
    -
    -    info["meta version"] = 2
    -    self.meta["piece layers"] = self.piece_layers
    -
    -
    -

    @@ -18325,12 +7555,12 @@
    -

    +

    utils -

    +

    @@ -18361,12 +7591,12 @@

    -

    +

    Memo -

    +

    @@ -18399,86 +7629,6 @@

    -
    - Source code in torrentfile\utils.py -
    41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    -77
    -78
    class Memo:
    -    """
    -    Memoice chache object.
    -
    -    Parameters
    -    ----------
    -    func : function
    -        The function that is being memoized.
    -    """
    -
    -    def __init__(self, func):
    -        """
    -        Construct for memoization.
    -        """
    -        self.func = func
    -        self.counter = 0
    -        self.cache = {}
    -
    -    def __call__(self, path):
    -        """
    -        Invoke each time memo function is called.
    -
    -        Parameters
    -        ----------
    -        path : str
    -            The relative or absolute path being used as key in cache dict.
    -
    -        Returns
    -        -------
    -        Any :
    -            The results of calling the function with path.
    -        """
    -        if path in self.cache and os.path.exists(path):
    -            self.counter += 1
    -            return self.cache[path]
    -        result = self.func(path)
    -        self.cache[path] = result
    -        return result
    -
    -
    -
    @@ -18498,11 +7648,11 @@

    -

    +

    __call__(path) -

    +

    @@ -18550,50 +7700,6 @@
    -
    - Source code in torrentfile\utils.py -
    59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    -77
    -78
    def __call__(self, path):
    -    """
    -    Invoke each time memo function is called.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        The relative or absolute path being used as key in cache dict.
    -
    -    Returns
    -    -------
    -    Any :
    -        The results of calling the function with path.
    -    """
    -    if path in self.cache and os.path.exists(path):
    -        self.counter += 1
    -        return self.cache[path]
    -    result = self.func(path)
    -    self.cache[path] = result
    -    return result
    -
    -
    -
    @@ -18604,35 +7710,17 @@
    -
    +

    __init__(func) -

    +

    Construct for memoization.

    -
    - Source code in torrentfile\utils.py -
    51
    -52
    -53
    -54
    -55
    -56
    -57
    def __init__(self, func):
    -    """
    -    Construct for memoization.
    -    """
    -    self.func = func
    -    self.counter = 0
    -    self.cache = {}
    -
    -
    -
    @@ -18653,12 +7741,12 @@
    -

    +

    MissingPathError -

    +

    @@ -18694,50 +7782,6 @@

    -
    - Source code in torrentfile\utils.py -
     81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    class MissingPathError(Exception):
    -    """
    -    Path parameter is required to specify target content.
    -
    -    Creating a .torrent file with no contents seems rather silly.
    -
    -    Parameters
    -    ----------
    -    message : str
    -        Message for user (optional).
    -    """
    -
    -    def __init__(self, message: str = None):
    -        """
    -        Raise when creating a meta file without specifying target content.
    -
    -        The `message` argument is a message to pass to Exception base class.
    -        """
    -        self.message = f"Path arguement is missing and required {str(message)}"
    -        super().__init__(message)
    -
    -
    -
    @@ -18755,11 +7799,11 @@

    -

    +

    __init__(message=None) -

    +

    @@ -18767,26 +7811,6 @@

    Raise when creating a meta file without specifying target content.

    The message argument is a message to pass to Exception base class.

    -
    - Source code in torrentfile\utils.py -
     93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    def __init__(self, message: str = None):
    -    """
    -    Raise when creating a meta file without specifying target content.
    -
    -    The `message` argument is a message to pass to Exception base class.
    -    """
    -    self.message = f"Path arguement is missing and required {str(message)}"
    -    super().__init__(message)
    -
    -
    -
    @@ -18807,12 +7831,12 @@
    -

    +

    PieceLengthValueError -

    +

    @@ -18847,46 +7871,6 @@

    -
    - Source code in torrentfile\utils.py -
    103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    class PieceLengthValueError(Exception):
    -    """
    -    Piece Length parameter must equal a perfect power of 2.
    -
    -    Parameters
    -    ----------
    -    message : str
    -        Message for user (optional).
    -    """
    -
    -    def __init__(self, message: str = None):
    -        """
    -        Raise when creating a meta file with incorrect piece length value.
    -
    -        The `message` argument is a message to pass to Exception base class.
    -        """
    -        self.message = f"Incorrect value for piece length: {str(message)}"
    -        super().__init__(message)
    -
    -
    -
    @@ -18904,11 +7888,11 @@

    -

    +

    __init__(message=None) -

    +

    @@ -18916,26 +7900,6 @@
    - Source code in torrentfile\utils.py -
    113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    def __init__(self, message: str = None):
    -    """
    -    Raise when creating a meta file with incorrect piece length value.
    -
    -    The `message` argument is a message to pass to Exception base class.
    -    """
    -    self.message = f"Incorrect value for piece length: {str(message)}"
    -    super().__init__(message)
    -
    -
    -
    @@ -18957,11 +7921,11 @@
    +

    _filelist_total(path) -

    +
    @@ -19016,64 +7980,6 @@

    -
    - Source code in torrentfile\utils.py -
    228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    def _filelist_total(path: str) -> tuple:
    -    """
    -    Search directory tree for files.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to file or directory base
    -
    -    Returns
    -    -------
    -    int
    -        Sum of all filesizes in filelist.
    -    list
    -        All file paths within directory tree.
    -    """
    -    if path.is_file():
    -        file_size = os.path.getsize(path)
    -        return file_size, [str(path)]
    -    total = 0
    -    filelist = []
    -    if path.is_dir():
    -        for item in path.iterdir():
    -            size, paths = filelist_total(item)
    -            total += size
    -            filelist.extend(paths)
    -    return total, sorted(filelist)
    -
    -
    -

    @@ -19084,11 +7990,11 @@

    -

    +

    filelist_total(pathstring) -

    +
    @@ -19155,58 +8061,6 @@

    -
    - Source code in torrentfile\utils.py -
    202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    @Memo
    -def filelist_total(pathstring: str) -> os.PathLike:
    -    """
    -    Perform error checking and format conversion to os.PathLike.
    -
    -    Parameters
    -    ----------
    -    pathstring : str
    -        An existing filesystem path.
    -
    -    Returns
    -    -------
    -    os.PathLike
    -        Input path converted to bytes format.
    -
    -    Raises
    -    ------
    -    MissingPathError
    -        File could not be found.
    -    """
    -    if os.path.exists(pathstring):
    -        path = Path(pathstring)
    -        return _filelist_total(path)
    -    raise MissingPathError
    -
    -
    -

    @@ -19217,11 +8071,11 @@

    -

    +

    get_file_list(path) -

    +
    @@ -19270,42 +8124,6 @@

    -
    - Source code in torrentfile\utils.py -
    275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    def get_file_list(path: str) -> list:
    -    """
    -    Return a sorted list of file paths contained in directory.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        target file or directory.
    -
    -    Returns
    -    -------
    -    list
    -        sorted list of file paths.
    -    """
    -    _, filelist = filelist_total(path)
    -    return filelist
    -
    -
    -

    @@ -19316,11 +8134,11 @@

    -

    +

    get_piece_length(size) -

    +
    @@ -19369,46 +8187,6 @@

    -
    - Source code in torrentfile\utils.py -
    182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    def get_piece_length(size: int) -> int:
    -    """
    -    Calculate the ideal piece length for bittorrent data.
    -
    -    Parameters
    -    ----------
    -    size : int
    -        Total bits of all files incluided in .torrent file.
    -
    -    Returns
    -    -------
    -    int
    -        Ideal piece length.
    -    """
    -    exp = 14
    -    while size / (2**exp) > 200 and exp < 25:
    -        exp += 1
    -    return 2**exp
    -
    -
    -

    @@ -19419,11 +8197,11 @@

    -

    +

    humanize_bytes(amount) -

    +
    @@ -19472,52 +8250,6 @@

    -
    - Source code in torrentfile\utils.py -
    123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    def humanize_bytes(amount: int) -> str:
    -    """
    -    Convert integer into human readable memory sized denomination.
    -
    -    Parameters
    -    ----------
    -    amount : int
    -        total number of bytes.
    -
    -    Returns
    -    -------
    -    str
    -        human readable representation of the given amount of bytes.
    -    """
    -    if amount < 1024:
    -        return str(amount)
    -    if 1024 <= amount < 1_048_576:
    -        return f"{amount // 1024} KiB"
    -    if 1_048_576 <= amount < 1_073_741_824:
    -        return f"{amount // 1_048_576} MiB"
    -    return f"{amount // 1073741824} GiB"
    -
    -
    -

    @@ -19528,11 +8260,11 @@

    -

    +

    next_power_2(value) -

    +
    @@ -19581,50 +8313,6 @@

    -
    - Source code in torrentfile\utils.py -
    334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    def next_power_2(value: int) -> int:
    -    """
    -    Calculate the next perfect power of 2 equal to or greater than value.
    -
    -    Parameters
    -    ----------
    -    value : int
    -        integer value that is less than some perfect power of 2.
    -
    -    Returns
    -    -------
    -    int
    -        The next power of 2 greater than value, or value if already power of 2.
    -    """
    -    if not value & (value - 1) and value:
    -        return value
    -    start = 1
    -    while start < value:
    -        start <<= 1
    -    return start
    -
    -
    -

    @@ -19635,11 +8323,11 @@

    -

    +

    normalize_piece_length(piece_length) -

    +
    @@ -19706,78 +8394,6 @@

    -
    - Source code in torrentfile\utils.py -
    146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    def normalize_piece_length(piece_length: int) -> int:
    -    """
    -    Verify input piece_length is valid and convert accordingly.
    -
    -    Parameters
    -    ----------
    -    piece_length : int | str
    -        The piece length provided by user.
    -
    -    Returns
    -    -------
    -    int
    -        normalized piece length.
    -
    -    Raises
    -    ------
    -    PieceLengthValueError :
    -        If piece length is improper value.
    -    """
    -    if isinstance(piece_length, str):
    -        if piece_length.isnumeric():
    -            piece_length = int(piece_length)
    -        else:
    -            raise PieceLengthValueError(piece_length)
    -
    -    if 13 < piece_length < 26:
    -        return 2**piece_length
    -    if piece_length <= 13:
    -        raise PieceLengthValueError(piece_length)
    -
    -    log = int(math.log2(piece_length))
    -    if 2**log == piece_length:
    -        return piece_length
    -    raise PieceLengthValueError
    -
    -
    -

    @@ -19788,11 +8404,11 @@

    -

    +

    path_piece_length(path) -

    +
    @@ -19841,42 +8457,6 @@

    -
    - Source code in torrentfile\utils.py -
    316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    def path_piece_length(path: str) -> int:
    -    """
    -    Calculate piece length for input path and contents.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        The absolute path to directory and contents.
    -
    -    Returns
    -    -------
    -    int
    -        The size of pieces of torrent content.
    -    """
    -    psize = path_size(path)
    -    return get_piece_length(psize)
    -
    -
    -

    @@ -19887,11 +8467,11 @@

    -

    +

    path_size(path) -

    +
    @@ -19940,42 +8520,6 @@

    -
    - Source code in torrentfile\utils.py -
    257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    def path_size(path: str) -> int:
    -    """
    -    Return the total size of all files in path recursively.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        path to target file or directory.
    -
    -    Returns
    -    -------
    -    int
    -        total size of files.
    -    """
    -    total_size, _ = filelist_total(path)
    -    return total_size
    -
    -
    -

    @@ -19986,11 +8530,11 @@

    -

    +

    path_stat(path) -

    +
    @@ -20051,52 +8595,6 @@

    -
    - Source code in torrentfile\utils.py -
    293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    def path_stat(path: str) -> tuple:
    -    """
    -    Calculate directory statistics.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        The path to start calculating from.
    -
    -    Returns
    -    -------
    -    list
    -        List of all files contained in Directory
    -    int
    -        Total sum of bytes from all contents of dir
    -    int
    -        The size of pieces of the torrent contents.
    -    """
    -    total_size, filelist = filelist_total(path)
    -    piece_length = get_piece_length(total_size)
    -    return (filelist, total_size, piece_length)
    -
    -
    -

    @@ -20117,12 +8615,12 @@

    -

    +

    version -

    +
    @@ -20161,15 +8659,7 @@

    - - -

    - torrentfile.cli - - - -

    - +

    Command Line Interface for TorrentFile project.

    @@ -20212,202 +8702,6 @@

    Subclasses Argparse.HelpFormatter.

    -
    - Source code in torrentfile\cli.py -
     70
    - 71
    - 72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    class TorrentFileHelpFormatter(HelpFormatter):
    -    """
    -    Formatting class for help tips provided by the CLI.
    -
    -    Subclasses Argparse.HelpFormatter.
    -    """
    -
    -    def __init__(self, prog, width=40, max_help_positions=30):
    -        """
    -        Construct HelpFormat class for usage output.
    -
    -        Parameters
    -        ----------
    -        prog : str
    -            Name of the program.
    -        width : int
    -            Max width of help message output.
    -        max_help_positions : int
    -            max length until line wrap.
    -        """
    -        super().__init__(
    -            prog, width=width, max_help_position=max_help_positions
    -        )
    -
    -    def _split_lines(self, text, _):
    -        """
    -        Split multiline help messages and remove indentation.
    -
    -        Parameters
    -        ----------
    -        text : str
    -            text that needs to be split
    -        _ : int
    -            max width for line.
    -        """
    -        lines = text.split("\n")
    -        return [line.strip() for line in lines if line]
    -
    -    def _format_text(self, text):
    -        """
    -        Format text for cli usage messages.
    -
    -        Parameters
    -        ----------
    -        text : str
    -            Pre-formatted text.
    -
    -        Returns
    -        -------
    -        str
    -            Formatted text from input.
    -        """
    -        text = text % dict(prog=self._prog) if "%(prog)" in text else text
    -        text = self._whitespace_matcher.sub(" ", text).strip()
    -        return text + "\n\n"
    -
    -    def _join_parts(self, part_strings):
    -        """
    -        Combine different sections of the help message.
    -
    -        Parameters
    -        ----------
    -        part_strings : list
    -            List of argument help messages and headers.
    -
    -        Returns
    -        -------
    -        str
    -            Fully formatted help message for CLI.
    -        """
    -        parts = self.format_headers(part_strings)
    -        return super()._join_parts(parts)
    -
    -    @staticmethod
    -    def format_headers(parts):
    -        """
    -        Format help message section headers.
    -
    -        Parameters
    -        ----------
    -        parts : list
    -            List of individual lines for help message.
    -
    -        Returns
    -        -------
    -        list
    -            Input list with formatted section headers.
    -        """
    -        if parts and parts[0].startswith("usage:"):
    -            parts[0] = "Usage\n=====\n  " + parts[0][6:]
    -        headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
    -        for i in headings[::-1]:
    -            parts[i] = parts[i][:-2].title()
    -            underline = "".join(["\n", "-" * len(parts[i]), "\n"])
    -            parts.insert(i + 1, underline)
    -        return parts
    -
    -
    -
    @@ -20479,42 +8773,6 @@

    - Source code in torrentfile\cli.py -
    77
    -78
    -79
    -80
    -81
    -82
    -83
    -84
    -85
    -86
    -87
    -88
    -89
    -90
    -91
    -92
    def __init__(self, prog, width=40, max_help_positions=30):
    -    """
    -    Construct HelpFormat class for usage output.
    -
    -    Parameters
    -    ----------
    -    prog : str
    -        Name of the program.
    -    width : int
    -        Max width of help message output.
    -    max_help_positions : int
    -        max length until line wrap.
    -    """
    -    super().__init__(
    -        prog, width=width, max_help_position=max_help_positions
    -    )
    -
    -
    -

    @@ -20578,44 +8836,6 @@

    - Source code in torrentfile\cli.py -
    108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    def _format_text(self, text):
    -    """
    -    Format text for cli usage messages.
    -
    -    Parameters
    -    ----------
    -    text : str
    -        Pre-formatted text.
    -
    -    Returns
    -    -------
    -    str
    -        Formatted text from input.
    -    """
    -    text = text % dict(prog=self._prog) if "%(prog)" in text else text
    -    text = self._whitespace_matcher.sub(" ", text).strip()
    -    return text + "\n\n"
    -
    -
    -

    @@ -20679,42 +8899,6 @@

    - Source code in torrentfile\cli.py -
    126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    def _join_parts(self, part_strings):
    -    """
    -    Combine different sections of the help message.
    -
    -    Parameters
    -    ----------
    -    part_strings : list
    -        List of argument help messages and headers.
    -
    -    Returns
    -    -------
    -    str
    -        Fully formatted help message for CLI.
    -    """
    -    parts = self.format_headers(part_strings)
    -    return super()._join_parts(parts)
    -
    -
    - @@ -20770,36 +8954,6 @@

    - Source code in torrentfile\cli.py -
     94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    def _split_lines(self, text, _):
    -    """
    -    Split multiline help messages and remove indentation.
    -
    -    Parameters
    -    ----------
    -    text : str
    -        text that needs to be split
    -    _ : int
    -        max width for line.
    -    """
    -    lines = text.split("\n")
    -    return [line.strip() for line in lines if line]
    -
    -
    - @@ -20866,56 +9020,6 @@

    - Source code in torrentfile\cli.py -
    143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    @staticmethod
    -def format_headers(parts):
    -    """
    -    Format help message section headers.
    -
    -    Parameters
    -    ----------
    -    parts : list
    -        List of individual lines for help message.
    -
    -    Returns
    -    -------
    -    list
    -        Input list with formatted section headers.
    -    """
    -    if parts and parts[0].startswith("usage:"):
    -        parts[0] = "Usage\n=====\n  " + parts[0][6:]
    -    headings = [i for i in range(len(parts)) if parts[i].endswith(":\n")]
    -    for i in headings[::-1]:
    -        parts[i] = parts[i][:-2].title()
    -        underline = "".join(["\n", "-" * len(parts[i]), "\n"])
    -        parts.insert(i + 1, underline)
    -    return parts
    -
    -
    - @@ -20948,66 +9052,6 @@

    Activate the builtin logging mechanism when passed debug flag from CLI.

    -
    - Source code in torrentfile\cli.py -
    40
    -41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    def activate_logger():
    -    """
    -    Activate the builtin logging mechanism when passed debug flag from CLI.
    -    """
    -    logging.basicConfig(level=logging.INFO)
    -    logger = logging.getLogger()
    -    # file_handler = logging.FileHandler(
    -    #     "torrentfile.log", mode="a+", encoding="utf-8"
    -    # )
    -    console_handler = logging.StreamHandler(stream=sys.stderr)
    -    # file_formatter = logging.Formatter(
    -    #     "%(asctime)s %(levelno)s %(message)s",
    -    #     datefmt="%m-%d %H:%M:%S",
    -    #     style="%",
    -    # )
    -    stream_formatter = logging.Formatter(
    -        "%(asctime)s %(levelno)s %(message)s",
    -        datefmt="%m-%d %H:%M:%S",
    -        style="%",
    -    )
    -    # file_handler.setFormatter(file_formatter)
    -    console_handler.setFormatter(stream_formatter)
    -    # file_handler.setLevel(logging.INFO)
    -    console_handler.setLevel(logging.DEBUG)
    -    logger.setLevel(logging.DEBUG)
    -    logger.addHandler(console_handler)
    -    # logger.addHandler(file_handler)
    -    logger.debug("Debug: ON")
    -
    -
    -
    @@ -21071,716 +9115,6 @@

    -
    - Source code in torrentfile\cli.py -
    168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    -403
    -404
    -405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    -519
    -520
    def execute(args=None) -> list:
    -    """
    -    Initialize Command Line Interface for torrentfile.
    -
    -    Parameters
    -    ----------
    -    args : list
    -        Commandline arguments. default=None
    -
    -    Returns
    -    -------
    -    list
    -        Depends on what the command line args were.
    -    """
    -    if not args:
    -        if sys.argv[1:]:
    -            args = sys.argv[1:]
    -        else:
    -            args = ["-h"]
    -
    -    parser = ArgumentParser(
    -        "torrentfile",
    -        description=(
    -            "Command line tools for creating, editing, checking and "
    -            "interacting with Bittorrent metainfo files"
    -        ),
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -        conflict_handler="resolve",
    -    )
    -
    -    parser.add_argument(
    -        "-i",
    -        "--interactive",
    -        action="store_true",
    -        dest="interactive",
    -        help="select program options interactively",
    -    )
    -
    -    parser.add_argument(
    -        "-V",
    -        "--version",
    -        action="version",
    -        version=f"torrentfile v{version}",
    -        help="show program version and exit",
    -    )
    -
    -    parser.add_argument(
    -        "-v",
    -        "--verbose",
    -        action="store_true",
    -        dest="debug",
    -        help="output debug information",
    -    )
    -
    -    parser.set_defaults(func=parser.print_help)
    -
    -    subparsers = parser.add_subparsers(
    -        title="Actions",
    -        dest="command",
    -        metavar="create, edit, magnet, recheck",
    -    )
    -
    -    create_parser = subparsers.add_parser(
    -        "create",
    -        help="""Generate a new torrent meta file.""",
    -        prefix_chars="-",
    -        aliases=["c", "new"],
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    create_parser.add_argument(
    -        "-a",
    -        "-t",
    -        "--announce",
    -        "--tracker",
    -        action="store",
    -        dest="announce",
    -        metavar="<url>",
    -        nargs="+",
    -        default=[],
    -        help="One or more space-seperated torrent tracker url(s).",
    -    )
    -
    -    create_parser.add_argument(
    -        "-p",
    -        "--private",
    -        action="store_true",
    -        dest="private",
    -        help="Creates private torrent with multi-tracker and DHT turned off.",
    -    )
    -
    -    create_parser.add_argument(
    -        "-s",
    -        "--source",
    -        action="store",
    -        dest="source",
    -        metavar="<source>",
    -        help="Add a source string. Useful for cross-seeding.",
    -    )
    -
    -    create_parser.add_argument(
    -        "-m",
    -        "--magnet",
    -        action="store_true",
    -        dest="magnet",
    -    )
    -
    -    create_parser.add_argument(
    -        "-c",
    -        "--comment",
    -        action="store",
    -        dest="comment",
    -        metavar="<comment>",
    -        help="Include a comment in file metadata",
    -    )
    -
    -    create_parser.add_argument(
    -        "-o",
    -        "--out",
    -        action="store",
    -        dest="outfile",
    -        metavar="<path>",
    -        help="Output save path for created .torrent file",
    -    )
    -
    -    create_parser.add_argument(
    -        "--cwd",
    -        "--current",
    -        action="store_true",
    -        dest="cwd",
    -        help="Save output .torrent file to current directory",
    -    )
    -
    -    create_parser.add_argument(
    -        "--prog",
    -        "--progress",
    -        default="1",
    -        action="store",
    -        dest="progress",
    -        help="""
    -        Set the progress bar level.
    -        Options = 0, 1
    -        (0) = Do not display progress bar.
    -        (1) = Display progress bar.
    -        """,
    -    )
    -
    -    create_parser.add_argument(
    -        "--meta-version",
    -        default="1",
    -        choices=["1", "2", "3"],
    -        action="store",
    -        dest="meta_version",
    -        metavar="<int>",
    -        help="""
    -        Bittorrent metafile version.
    -        Options = 1, 2, 3
    -        (1) = Bittorrent v1 (Default)
    -        (2) = Bittorrent v2
    -        (3) = Bittorrent v1 & v2 hybrid
    -        """,
    -    )
    -
    -    create_parser.add_argument(
    -        "--piece-length",
    -        action="store",
    -        dest="piece_length",
    -        metavar="<int>",
    -        help="""
    -        (Default: <blank>) Number of bytes for per chunk of data transmitted
    -        by Bittorrent client. Acceptable values include integers 14-26 which
    -        will be interpreted as a perfect power of 2.  e.g. 14 = 16KiB pieces.
    -        Examples:: [--piece-length 14] [--piece-length 20]
    -        """,
    -    )
    -
    -    create_parser.add_argument(
    -        "-w",
    -        "--web-seed",
    -        action="store",
    -        dest="url_list",
    -        metavar="<url>",
    -        nargs="+",
    -        help="list of web addresses where torrent data exists (GetRight).",
    -    )
    -
    -    create_parser.add_argument(
    -        "--http-seed",
    -        action="store",
    -        dest="httpseeds",
    -        metavar="<url>",
    -        nargs="+",
    -        help="list of URLs, addresses where content can be found (Hoffman).",
    -    )
    -
    -    create_parser.add_argument(
    -        "content",
    -        action="store",
    -        metavar="<content>",
    -        nargs="?",
    -        help="Path to content file or directory",
    -    )
    -
    -    create_parser.set_defaults(func=create)
    -
    -    edit_parser = subparsers.add_parser(
    -        "edit",
    -        help="""
    -        Edit existing torrent meta file.
    -        """,
    -        aliases=["e"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    edit_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        help="path to *.torrent file",
    -        metavar="<*.torrent>",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--tracker",
    -        action="store",
    -        dest="announce",
    -        metavar="<url>",
    -        nargs="+",
    -        help="""
    -        Replace current list of tracker/announce urls with one or more space
    -        seperated Bittorrent tracker announce url(s).
    -        """,
    -    )
    -
    -    edit_parser.add_argument(
    -        "--web-seed",
    -        action="store",
    -        dest="url_list",
    -        metavar="<url>",
    -        nargs="+",
    -        help="Replace current list of web-seed urls with one or more url(s)",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--http-seed",
    -        action="store",
    -        dest="httpseeds",
    -        metavar="<url>",
    -        nargs="+",
    -        help="replace all currently listed addresses with new list (Hoffman).",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--private",
    -        action="store_true",
    -        help="Make torrent private.",
    -        dest="private",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--comment",
    -        help="Replaces any existing comment with <comment>",
    -        metavar="<comment>",
    -        dest="comment",
    -        action="store",
    -    )
    -
    -    edit_parser.add_argument(
    -        "--source",
    -        action="store",
    -        dest="source",
    -        metavar="<source>",
    -        help="Replaces current source with <source>",
    -    )
    -
    -    edit_parser.set_defaults(func=edit)
    -
    -    magnet_parser = subparsers.add_parser(
    -        "magnet",
    -        help="""
    -        Generate magnet url from an existing Bittorrent meta file.
    -        """,
    -        aliases=["m"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    magnet_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        help="Path to Bittorrent meta file.",
    -        metavar="<*.torrent>",
    -    )
    -
    -    magnet_parser.set_defaults(func=magnet)
    -
    -    check_parser = subparsers.add_parser(
    -        "recheck",
    -        help="""
    -        Calculate amount of torrent meta file's content is found on disk.
    -        """,
    -        aliases=["r", "check"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    check_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        metavar="<*.torrent>",
    -        help="path to .torrent file.",
    -    )
    -
    -    check_parser.add_argument(
    -        "content",
    -        action="store",
    -        metavar="<content>",
    -        help="path to content file or directory",
    -    )
    -
    -    check_parser.set_defaults(func=recheck)
    -
    -    info_parser = subparsers.add_parser(
    -        "info",
    -        help="""
    -        Show detailed information about a torrent file.
    -        """,
    -        aliases=["i"],
    -        prefix_chars="-",
    -        formatter_class=TorrentFileHelpFormatter,
    -    )
    -
    -    info_parser.add_argument(
    -        "metafile",
    -        action="store",
    -        metavar="<*.torrent>",
    -        help="path to pre-existing torrent file.",
    -    )
    -
    -    info_parser.set_defaults(func=info)
    -
    -    args = parser.parse_args(args)
    -
    -    if args.debug:
    -        activate_logger()
    -
    -    if args.interactive:
    -        return select_action()
    -
    -    if hasattr(args, "func"):
    -        return args.func(args)
    -    return args
    -
    -
    -
    @@ -21802,20 +9136,6 @@

    Initiate main function for CLI script.

    -
    - Source code in torrentfile\cli.py -
    526
    -527
    -528
    -529
    -530
    def main():
    -    """
    -    Initiate main function for CLI script.
    -    """
    -    execute()
    -
    -
    -
    @@ -21834,15 +9154,7 @@

    - - -

    - torrentfile.edit - - - -

    - +

    Edit torrent module.

    @@ -21938,126 +9250,6 @@

    -
    - Source code in torrentfile\edit.py -
     73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    def edit_torrent(metafile: str, args: dict) -> dict:
    -    """
    -    Edit the properties and values in a torrent meta file.
    -
    -    Parameters
    -    ----------
    -    metafile : str
    -        path to the torrent meta file.
    -    args : dict
    -        key value pairs of the properties to be edited.
    -
    -    Returns
    -    -------
    -    dict
    -        The edited and nested Meta and info dictionaries.
    -    """
    -    logger.debug("editing torrent file %s", metafile)
    -    meta = pyben.load(metafile)
    -    info = meta["info"]
    -    filter_empty(args, meta, info)
    -
    -    if "comment" in args:
    -        info["comment"] = args["comment"]
    -
    -    if "source" in args:
    -        info["source"] = args["source"]
    -
    -    if "private" in args:
    -        info["private"] = 1
    -
    -    if "announce" in args:
    -        val = args.get("announce", None)
    -        if isinstance(val, str):
    -            vallist = val.split()
    -            meta["announce"] = vallist[0]
    -            meta["announce-list"] = [vallist]
    -        elif isinstance(val, list):
    -            meta["announce"] = val[0]
    -            meta["announce-list"] = [val]
    -
    -    if "url-list" in args:
    -        val = args.get("url-list")
    -        if isinstance(val, str):
    -            meta["url-list"] = val.split()
    -        elif isinstance(val, list):
    -            meta["url-list"] = val
    -
    -    if "httpseeds" in args:
    -        val = args.get("httpseeds")
    -        if isinstance(val, str):
    -            meta["httpseeds"] = val.split()
    -        elif isinstance(val, list):
    -            meta["httpseeds"] = val
    -
    -    meta["info"] = info
    -    os.remove(metafile)
    -    pyben.dump(meta, metafile)
    -    return meta
    -
    -
    -

    @@ -22123,60 +9315,6 @@

    -
    - Source code in torrentfile\edit.py -
    46
    -47
    -48
    -49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    def filter_empty(args: dict, meta: dict, info: dict):
    -    """
    -    Remove dictionary keys with empty values.
    -
    -    Parameters
    -    ----------
    -    args : dict
    -        Editable metafile properties from user.
    -    meta : dict
    -        Metafile data dictionary.
    -    info : dict
    -        Metafile info dictionary.
    -    """
    -    for key, val in list(args.items()):
    -        if val is None:
    -            del args[key]
    -            continue
    -
    -        if val == "":
    -            if key in meta:
    -                del meta[key]
    -            elif key in info:
    -                del info[key]
    -            del args[key]
    -            logger.debug("removeing empty fields %s", val)
    -
    -
    -
    @@ -22195,15 +9333,7 @@

    - - -

    - torrentfile.hasher - - - -

    - +

    Piece/File Hashers for Bittorrent meta file contents.

    @@ -22288,280 +9418,6 @@

    -
    - Source code in torrentfile\hasher.py -
    400
    -401
    -402
    -403
    -404
    -405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    class FileHasher(CbMixin, ProgMixin):
    -    """
    -    Calculate root and piece hashes for creating hybrid torrent file.
    -
    -    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
    -    With a branching factor of 2, merge layer hashes until blocks equal
    -    piece_length bytes for the piece layer, and then the root hash.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        path to target file.
    -    piece_length : int
    -        piece length for data chunks.
    -    progress : int
    -        default = None
    -    """
    -
    -    def __init__(
    -        self,
    -        path: str,
    -        piece_length: int,
    -        progress: bool = True,
    -        hybrid: bool = False,
    -    ):
    -        """
    -        Construct Hasher class instances for each file in torrent.
    -        """
    -        self.path = path
    -        self.piece_length = piece_length
    -        self.pieces = []
    -        self.layer_hashes = []
    -        self.piece_layer = None
    -        self.root = None
    -        self.padding_piece = None
    -        self.padding_file = None
    -        self.amount = piece_length // BLOCK_SIZE
    -        self.end = False
    -        self.current = open(path, "rb")
    -        self.hybrid = hybrid
    -        if progress:
    -            self.progressbar = True
    -            self.prog_start(os.path.getsize(path), path, unit="bytes")
    -
    -    def __iter__(self):
    -        """Return `self`: needed to implement iterator implementation."""
    -        return self
    -
    -    def _pad_remaining(self, block_count: int):
    -        """
    -        Generate Hash sized, 0 filled bytes for padding.
    -
    -        Parameters
    -        ----------
    -        block_count : int
    -            current total number of blocks collected.
    -
    -        Returns
    -        -------
    -        padding : bytes
    -            Padding to fill remaining portion of tree.
    -        """
    -        # when the there is only one block for file
    -        remaining = self.amount - block_count
    -        if not self.layer_hashes:
    -            power2 = next_power_2(block_count)
    -            remaining = power2 - block_count
    -        return [bytes(HASH_SIZE) for _ in range(remaining)]
    -
    -    def __next__(self) -> bytes:
    -        """
    -        Calculate layer hashes for contents of file.
    -
    -        Returns
    -        -------
    -        bytes
    -            The layer merckle root hash.
    -        """
    -        if self.end:
    -            self.end = False
    -            raise StopIteration
    -        plength = self.piece_length
    -        blocks = []
    -        piece = sha1()  # nosec
    -        total = 0
    -        block = bytearray(BLOCK_SIZE)
    -        for _ in range(self.amount):
    -            size = self.current.readinto(block)
    -            if not size:
    -                self.end = True
    -                break
    -            total += size
    -            plength -= size
    -            blocks.append(sha256(block[:size]).digest())
    -            if self.hybrid:
    -                piece.update(block[:size])
    -        if len(blocks) != self.amount:
    -            padding = self._pad_remaining(len(blocks))
    -            blocks.extend(padding)
    -        self.prog_update(total)
    -        layer_hash = merkle_root(blocks)
    -        self.layer_hashes.append(layer_hash)
    -        if self._cb:
    -            self._cb(layer_hash)
    -        if self.end:
    -            self._calculate_root()
    -            self.prog_close()
    -        if self.hybrid:
    -            if plength > 0:
    -                self.padding_file = {
    -                    "attr": "p",
    -                    "length": plength,
    -                    "path": [".pad", str(plength)],
    -                }
    -                piece.update(bytes(plength))
    -            piece = piece.digest()
    -            self.pieces.append(piece)
    -            return layer_hash, piece
    -        return layer_hash
    -
    -    def _calculate_root(self):
    -        """
    -        Calculate the root hash for opened file.
    -        """
    -        self.piece_layer = b"".join(self.layer_hashes)
    -
    -        if len(self.layer_hashes) > 1:
    -            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
    -
    -            pow2 = next_power_2(len(self.layer_hashes))
    -            remainder = pow2 - len(self.layer_hashes)
    -
    -            self.layer_hashes += [pad_piece for _ in range(remainder)]
    -        self.root = merkle_root(self.layer_hashes)
    -        self.current.close()
    -
    -
    -
    @@ -22602,60 +9458,6 @@

    Construct Hasher class instances for each file in torrent.

    -
    - Source code in torrentfile\hasher.py -
    418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    def __init__(
    -    self,
    -    path: str,
    -    piece_length: int,
    -    progress: bool = True,
    -    hybrid: bool = False,
    -):
    -    """
    -    Construct Hasher class instances for each file in torrent.
    -    """
    -    self.path = path
    -    self.piece_length = piece_length
    -    self.pieces = []
    -    self.layer_hashes = []
    -    self.piece_layer = None
    -    self.root = None
    -    self.padding_piece = None
    -    self.padding_file = None
    -    self.amount = piece_length // BLOCK_SIZE
    -    self.end = False
    -    self.current = open(path, "rb")
    -    self.hybrid = hybrid
    -    if progress:
    -        self.progressbar = True
    -        self.prog_start(os.path.getsize(path), path, unit="bytes")
    -
    -
    -

    @@ -22677,16 +9479,6 @@

    Return self: needed to implement iterator implementation.

    -
    - Source code in torrentfile\hasher.py -
    444
    -445
    -446
    def __iter__(self):
    -    """Return `self`: needed to implement iterator implementation."""
    -    return self
    -
    -
    -
    @@ -22726,110 +9518,6 @@

    -
    - Source code in torrentfile\hasher.py -
    469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    def __next__(self) -> bytes:
    -    """
    -    Calculate layer hashes for contents of file.
    -
    -    Returns
    -    -------
    -    bytes
    -        The layer merckle root hash.
    -    """
    -    if self.end:
    -        self.end = False
    -        raise StopIteration
    -    plength = self.piece_length
    -    blocks = []
    -    piece = sha1()  # nosec
    -    total = 0
    -    block = bytearray(BLOCK_SIZE)
    -    for _ in range(self.amount):
    -        size = self.current.readinto(block)
    -        if not size:
    -            self.end = True
    -            break
    -        total += size
    -        plength -= size
    -        blocks.append(sha256(block[:size]).digest())
    -        if self.hybrid:
    -            piece.update(block[:size])
    -    if len(blocks) != self.amount:
    -        padding = self._pad_remaining(len(blocks))
    -        blocks.extend(padding)
    -    self.prog_update(total)
    -    layer_hash = merkle_root(blocks)
    -    self.layer_hashes.append(layer_hash)
    -    if self._cb:
    -        self._cb(layer_hash)
    -    if self.end:
    -        self._calculate_root()
    -        self.prog_close()
    -    if self.hybrid:
    -        if plength > 0:
    -            self.padding_file = {
    -                "attr": "p",
    -                "length": plength,
    -                "path": [".pad", str(plength)],
    -            }
    -            piece.update(bytes(plength))
    -        piece = piece.digest()
    -        self.pieces.append(piece)
    -        return layer_hash, piece
    -    return layer_hash
    -
    -
    -
    @@ -22851,40 +9539,6 @@

    Calculate the root hash for opened file.

    -
    - Source code in torrentfile\hasher.py -
    520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    def _calculate_root(self):
    -    """
    -    Calculate the root hash for opened file.
    -    """
    -    self.piece_layer = b"".join(self.layer_hashes)
    -
    -    if len(self.layer_hashes) > 1:
    -        pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
    -
    -        pow2 = next_power_2(len(self.layer_hashes))
    -        remainder = pow2 - len(self.layer_hashes)
    -
    -        self.layer_hashes += [pad_piece for _ in range(remainder)]
    -    self.root = merkle_root(self.layer_hashes)
    -    self.current.close()
    -
    -
    -
    @@ -22948,50 +9602,6 @@

    -
    - Source code in torrentfile\hasher.py -
    448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    def _pad_remaining(self, block_count: int):
    -    """
    -    Generate Hash sized, 0 filled bytes for padding.
    -
    -    Parameters
    -    ----------
    -    block_count : int
    -        current total number of blocks collected.
    -
    -    Returns
    -    -------
    -    padding : bytes
    -        Padding to fill remaining portion of tree.
    -    """
    -    # when the there is only one block for file
    -    remaining = self.amount - block_count
    -    if not self.layer_hashes:
    -        power2 = next_power_2(block_count)
    -        remaining = power2 - block_count
    -    return [bytes(HASH_SIZE) for _ in range(remaining)]
    -
    -
    -
    @@ -23075,226 +9685,6 @@

    -
    - Source code in torrentfile\hasher.py -
     36
    - 37
    - 38
    - 39
    - 40
    - 41
    - 42
    - 43
    - 44
    - 45
    - 46
    - 47
    - 48
    - 49
    - 50
    - 51
    - 52
    - 53
    - 54
    - 55
    - 56
    - 57
    - 58
    - 59
    - 60
    - 61
    - 62
    - 63
    - 64
    - 65
    - 66
    - 67
    - 68
    - 69
    - 70
    - 71
    - 72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    class Hasher(CbMixin, ProgMixin):
    -    """
    -    Piece hasher for Bittorrent V1 files.
    -
    -    Takes a sorted list of all file paths, calculates sha1 hash
    -    for fixed size pieces of file data from each file
    -    seemlessly until the last piece which may be smaller than others.
    -
    -    Parameters
    -    ----------
    -    paths : list
    -        List of files.
    -    piece_length : int
    -        Size of chuncks to split the data into.
    -    progress : int
    -        default = None
    -    """
    -
    -    def __init__(self, paths: list, piece_length: int, progress: bool = True):
    -        """Generate hashes of piece length data from filelist contents."""
    -        self.piece_length = piece_length
    -        self.paths = paths
    -        self.progress = progress
    -        self.total = sum([os.path.getsize(i) for i in self.paths])
    -        self.index = 0
    -        self.current = open(self.paths[0], "rb")
    -        if self.progress:
    -            total = os.path.getsize(self.paths[0])
    -            self.prog_start(total, self.paths[0], unit="bytes")
    -        logger.debug("Hashing %s", str(self.paths[0]))
    -
    -    def __iter__(self):
    -        """
    -        Iterate through feed pieces.
    -
    -        Returns
    -        -------
    -        self : iterator
    -            Iterator for leaves/hash pieces.
    -        """
    -        return self
    -
    -    def _handle_partial(self, arr: bytearray) -> bytearray:
    -        """
    -        Define the handling partial pieces that span 2 or more files.
    -
    -        Parameters
    -        ----------
    -        arr : bytearray
    -            Incomplete piece containing partial data
    -
    -        Returns
    -        -------
    -        digest : bytearray
    -            SHA1 digest of the complete piece.
    -        """
    -        while len(arr) < self.piece_length and self.next_file():
    -            target = self.piece_length - len(arr)
    -            temp = bytearray(target)
    -            size = self.current.readinto(temp)
    -            arr.extend(temp[:size])
    -            self.prog_update(size)
    -            if size == target:
    -                break
    -        return sha1(arr).digest()  # nosec
    -
    -    def next_file(self) -> bool:
    -        """
    -        Seemlessly transition to next file in file list.
    -
    -        Returns
    -        -------
    -        bool:
    -            True if there is a next file otherwise False.
    -        """
    -        self.index += 1
    -        self.prog_close()
    -        if self.index < len(self.paths):
    -            path = self.paths[self.index]
    -            logger.debug("Hashing %s", str(path))
    -            self.current.close()
    -            if self.progress:
    -                self.prog_start(os.path.getsize(path), path, unit="bytes")
    -            self.current = open(path, "rb")
    -            return True
    -        return False
    -
    -    def __next__(self) -> bytes:
    -        """
    -        Generate piece-length pieces of data from input file list.
    -
    -        Returns
    -        -------
    -        bytes
    -            SHA1 hash of the piece extracted.
    -        """
    -        while True:
    -            piece = bytearray(self.piece_length)
    -            size = self.current.readinto(piece)
    -            if size == 0:
    -                if not self.next_file():
    -                    raise StopIteration
    -            elif size < self.piece_length:
    -                self.prog_update(size)
    -                return self._handle_partial(piece[:size])
    -            else:
    -                self.prog_update(size)
    -                return sha1(piece).digest()  # nosec
    -
    -
    -
    @@ -23328,34 +9718,6 @@

    Generate hashes of piece length data from filelist contents.

    -
    - Source code in torrentfile\hasher.py -
    54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    def __init__(self, paths: list, piece_length: int, progress: bool = True):
    -    """Generate hashes of piece length data from filelist contents."""
    -    self.piece_length = piece_length
    -    self.paths = paths
    -    self.progress = progress
    -    self.total = sum([os.path.getsize(i) for i in self.paths])
    -    self.index = 0
    -    self.current = open(self.paths[0], "rb")
    -    if self.progress:
    -        total = os.path.getsize(self.paths[0])
    -        self.prog_start(total, self.paths[0], unit="bytes")
    -    logger.debug("Hashing %s", str(self.paths[0]))
    -
    -
    -
    @@ -23395,30 +9757,6 @@

    -
    - Source code in torrentfile\hasher.py -
    67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    def __iter__(self):
    -    """
    -    Iterate through feed pieces.
    -
    -    Returns
    -    -------
    -    self : iterator
    -        Iterator for leaves/hash pieces.
    -    """
    -    return self
    -
    -
    -
    @@ -23458,52 +9796,6 @@

    -
    - Source code in torrentfile\hasher.py -
    123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    def __next__(self) -> bytes:
    -    """
    -    Generate piece-length pieces of data from input file list.
    -
    -    Returns
    -    -------
    -    bytes
    -        SHA1 hash of the piece extracted.
    -    """
    -    while True:
    -        piece = bytearray(self.piece_length)
    -        size = self.current.readinto(piece)
    -        if size == 0:
    -            if not self.next_file():
    -                raise StopIteration
    -        elif size < self.piece_length:
    -            self.prog_update(size)
    -            return self._handle_partial(piece[:size])
    -        else:
    -            self.prog_update(size)
    -            return sha1(piece).digest()  # nosec
    -
    -
    -
    @@ -23567,56 +9859,6 @@

    -
    - Source code in torrentfile\hasher.py -
     78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    def _handle_partial(self, arr: bytearray) -> bytearray:
    -    """
    -    Define the handling partial pieces that span 2 or more files.
    -
    -    Parameters
    -    ----------
    -    arr : bytearray
    -        Incomplete piece containing partial data
    -
    -    Returns
    -    -------
    -    digest : bytearray
    -        SHA1 digest of the complete piece.
    -    """
    -    while len(arr) < self.piece_length and self.next_file():
    -        target = self.piece_length - len(arr)
    -        temp = bytearray(target)
    -        size = self.current.readinto(temp)
    -        arr.extend(temp[:size])
    -        self.prog_update(size)
    -        if size == target:
    -            break
    -    return sha1(arr).digest()  # nosec
    -
    -
    -
    @@ -23656,50 +9898,6 @@

    -
    - Source code in torrentfile\hasher.py -
    102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    def next_file(self) -> bool:
    -    """
    -    Seemlessly transition to next file in file list.
    -
    -    Returns
    -    -------
    -    bool:
    -        True if there is a next file otherwise False.
    -    """
    -    self.index += 1
    -    self.prog_close()
    -    if self.index < len(self.paths):
    -        path = self.paths[self.index]
    -        logger.debug("Hashing %s", str(path))
    -        self.current.close()
    -        if self.progress:
    -            self.prog_start(os.path.getsize(path), path, unit="bytes")
    -        self.current = open(path, "rb")
    -        return True
    -    return False
    -
    -
    -
    @@ -23784,262 +9982,6 @@

    -
    - Source code in torrentfile\hasher.py -
    272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    class HasherHybrid(CbMixin, ProgMixin):
    -    """
    -    Calculate root and piece hashes for creating hybrid torrent file.
    -
    -    **DEPRECATED**
    -
    -    Create merkle tree layers from sha256 hashed 16KiB blocks of contents.
    -    With a branching factor of 2, merge layer hashes until blocks equal
    -    piece_length bytes for the piece layer, and then the root hash.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        path to target file.
    -    piece_length : int
    -        piece length for data chunks.
    -    progress : int
    -        default = None
    -    """
    -
    -    def __init__(self, path: str, piece_length: int, progress: bool = True):
    -        """
    -        Construct Hasher class instances for each file in torrent.
    -
    -        **DEPRECATED**
    -        """
    -        self.path = path
    -        self.piece_length = piece_length
    -        self.pieces = []
    -        self.layer_hashes = []
    -        self.piece_layer = None
    -        self.root = None
    -        self.padding_piece = None
    -        self.padding_file = None
    -        if progress:
    -            self.prog_start(os.path.getsize(path), path, unit="bytes")
    -        self.amount = piece_length // BLOCK_SIZE
    -        with open(path, "rb") as data:
    -            self.process_file(data)
    -
    -    def _pad_remaining(self, block_count: int):
    -        """
    -        Generate Hash sized, 0 filled bytes for padding.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        block_count : int
    -            current total number of blocks collected.
    -
    -        Returns
    -        -------
    -        padding : bytes
    -            Padding to fill remaining portion of tree.
    -        """
    -        # when the there is only one block for file
    -        remaining = self.amount - block_count
    -        if not self.layer_hashes:
    -            power2 = next_power_2(block_count)
    -            remaining = power2 - block_count
    -        self.prog_update(HASH_SIZE * remaining)
    -        return [bytes(HASH_SIZE) for _ in range(remaining)]
    -
    -    def process_file(self, data: bytearray):
    -        """
    -        Calculate layer hashes for contents of file.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        data : BytesIO
    -            File opened in read mode.
    -        """
    -        while True:
    -            plength = self.piece_length
    -            blocks = []
    -            piece = sha1()  # nosec
    -            total = 0
    -            block = bytearray(BLOCK_SIZE)
    -            for _ in range(self.amount):
    -                size = data.readinto(block)
    -                self.prog_update(size)
    -                if not size:
    -                    break
    -                total += size
    -                plength -= size
    -                blocks.append(sha256(block[:size]).digest())
    -                piece.update(block[:size])
    -            if not blocks:
    -                break
    -            if len(blocks) != self.amount:
    -                padding = self._pad_remaining(len(blocks))
    -                blocks.extend(padding)
    -            layer_hash = merkle_root(blocks)
    -            if self._cb:
    -                self._cb(layer_hash)
    -            self.layer_hashes.append(layer_hash)
    -            if plength > 0:
    -                self.padding_file = {
    -                    "attr": "p",
    -                    "length": plength,
    -                    "path": [".pad", str(plength)],
    -                }
    -                piece.update(bytes(plength))
    -            self.pieces.append(piece.digest())  # nosec
    -        self._calculate_root()
    -        self.prog_close()
    -
    -    def _calculate_root(self):
    -        """
    -        Calculate the root hash for opened file.
    -
    -        **DEPRECATED**
    -        """
    -        self.piece_layer = b"".join(self.layer_hashes)
    -
    -        if len(self.layer_hashes) > 1:
    -            pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
    -
    -            pow2 = next_power_2(len(self.layer_hashes))
    -            remainder = pow2 - len(self.layer_hashes)
    -
    -            self.layer_hashes += [pad_piece for _ in range(remainder)]
    -        self.root = merkle_root(self.layer_hashes)
    -
    -
    -
    @@ -24077,48 +10019,6 @@

    Construct Hasher class instances for each file in torrent.

    DEPRECATED

    -
    - Source code in torrentfile\hasher.py -
    292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    def __init__(self, path: str, piece_length: int, progress: bool = True):
    -    """
    -    Construct Hasher class instances for each file in torrent.
    -
    -    **DEPRECATED**
    -    """
    -    self.path = path
    -    self.piece_length = piece_length
    -    self.pieces = []
    -    self.layer_hashes = []
    -    self.piece_layer = None
    -    self.root = None
    -    self.padding_piece = None
    -    self.padding_file = None
    -    if progress:
    -        self.prog_start(os.path.getsize(path), path, unit="bytes")
    -    self.amount = piece_length // BLOCK_SIZE
    -    with open(path, "rb") as data:
    -        self.process_file(data)
    -
    -
    -
    @@ -24141,42 +10041,6 @@

    Calculate the root hash for opened file.

    DEPRECATED

    -
    - Source code in torrentfile\hasher.py -
    382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    def _calculate_root(self):
    -    """
    -    Calculate the root hash for opened file.
    -
    -    **DEPRECATED**
    -    """
    -    self.piece_layer = b"".join(self.layer_hashes)
    -
    -    if len(self.layer_hashes) > 1:
    -        pad_piece = merkle_root([bytes(32) for _ in range(self.amount)])
    -
    -        pow2 = next_power_2(len(self.layer_hashes))
    -        remainder = pow2 - len(self.layer_hashes)
    -
    -        self.layer_hashes += [pad_piece for _ in range(remainder)]
    -    self.root = merkle_root(self.layer_hashes)
    -
    -
    -
    @@ -24241,56 +10105,6 @@

    -
    - Source code in torrentfile\hasher.py -
    312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    def _pad_remaining(self, block_count: int):
    -    """
    -    Generate Hash sized, 0 filled bytes for padding.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    block_count : int
    -        current total number of blocks collected.
    -
    -    Returns
    -    -------
    -    padding : bytes
    -        Padding to fill remaining portion of tree.
    -    """
    -    # when the there is only one block for file
    -    remaining = self.amount - block_count
    -    if not self.layer_hashes:
    -        power2 = next_power_2(block_count)
    -        remaining = power2 - block_count
    -    self.prog_update(HASH_SIZE * remaining)
    -    return [bytes(HASH_SIZE) for _ in range(remaining)]
    -
    -
    -
    @@ -24337,100 +10151,6 @@

    -
    - Source code in torrentfile\hasher.py -
    336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    def process_file(self, data: bytearray):
    -    """
    -    Calculate layer hashes for contents of file.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    data : BytesIO
    -        File opened in read mode.
    -    """
    -    while True:
    -        plength = self.piece_length
    -        blocks = []
    -        piece = sha1()  # nosec
    -        total = 0
    -        block = bytearray(BLOCK_SIZE)
    -        for _ in range(self.amount):
    -            size = data.readinto(block)
    -            self.prog_update(size)
    -            if not size:
    -                break
    -            total += size
    -            plength -= size
    -            blocks.append(sha256(block[:size]).digest())
    -            piece.update(block[:size])
    -        if not blocks:
    -            break
    -        if len(blocks) != self.amount:
    -            padding = self._pad_remaining(len(blocks))
    -            blocks.extend(padding)
    -        layer_hash = merkle_root(blocks)
    -        if self._cb:
    -            self._cb(layer_hash)
    -        self.layer_hashes.append(layer_hash)
    -        if plength > 0:
    -            self.padding_file = {
    -                "attr": "p",
    -                "length": plength,
    -                "path": [".pad", str(plength)],
    -            }
    -            piece.update(bytes(plength))
    -        self.pieces.append(piece.digest())  # nosec
    -    self._calculate_root()
    -    self.prog_close()
    -
    -
    -
    @@ -24516,212 +10236,6 @@

    -
    - Source code in torrentfile\hasher.py -
    169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    class HasherV2(CbMixin, ProgMixin):
    -    """
    -    Calculate the root hash and piece layers for file contents.
    -
    -    **DEPRECATED**
    -
    -    Iterates over 16KiB blocks of data from given file, hashes the data,
    -    then creates a hash tree from the individual block hashes until size of
    -    hashed data equals the piece-length.  Then continues the hash tree until
    -    root hash is calculated.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to file.
    -    piece_length : int
    -        Size of layer hashes pieces.
    -    progress : int
    -        default = None
    -    """
    -
    -    def __init__(self, path: str, piece_length: int, progress: bool = True):
    -        """
    -        Calculate and store hash information for specific file.
    -
    -        **DEPRECATED**
    -        """
    -        self.path = path
    -        self.root = None
    -        self.piece_layer = None
    -        self.layer_hashes = []
    -        self.piece_length = piece_length
    -        self.num_blocks = piece_length // BLOCK_SIZE
    -        if progress:
    -            self.prog_start(os.path.getsize(path), path, unit="bytes")
    -        with open(self.path, "rb") as fd:
    -            self.process_file(fd)
    -
    -    def process_file(self, fd: str):
    -        """
    -        Calculate hashes over 16KiB chuncks of file content.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        fd : TextIOWrapper
    -            Opened file in read mode.
    -        """
    -        while True:
    -            blocks = []
    -            leaf = bytearray(BLOCK_SIZE)
    -            # generate leaves of merkle tree
    -
    -            for _ in range(self.num_blocks):
    -                size = fd.readinto(leaf)
    -                self.prog_update(size)
    -                if not size:
    -                    break
    -                blocks.append(sha256(leaf[:size]).digest())
    -
    -            # blocks is empty mean eof
    -            if not blocks:
    -                break
    -            if len(blocks) != self.num_blocks:
    -                # when size of file doesn't fill the last block
    -                # when the file contains multiple pieces
    -                remaining = self.num_blocks - len(blocks)
    -                if not self.layer_hashes:
    -                    # when the there is only one block for file
    -                    power2 = next_power_2(len(blocks))
    -                    remaining = power2 - len(blocks)
    -
    -                # pad the the rest with zeroes to fill remaining space.
    -                padding = [bytes(32) for _ in range(remaining)]
    -                self.prog_update(HASH_SIZE * remaining)
    -                blocks.extend(padding)
    -            # calculate the root hash for the merkle tree up to piece-length
    -
    -            layer_hash = merkle_root(blocks)
    -            if self._cb:
    -                self._cb(layer_hash)
    -            self.layer_hashes.append(layer_hash)
    -        self._calculate_root()
    -        self.prog_close()
    -
    -    def _calculate_root(self):
    -        """
    -        Calculate root hash for the target file.
    -
    -        **DEPRECATED**
    -        """
    -        self.piece_layer = b"".join(self.layer_hashes)
    -        hashes = len(self.layer_hashes)
    -        if hashes > 1:
    -            pow2 = next_power_2(hashes)
    -            remainder = pow2 - hashes
    -            pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)]
    -            for _ in range(remainder):
    -                self.layer_hashes.append(merkle_root(pad_piece))
    -        self.root = merkle_root(self.layer_hashes)
    -
    -
    -
    @@ -24756,42 +10270,6 @@

    Calculate and store hash information for specific file.

    DEPRECATED

    -
    - Source code in torrentfile\hasher.py -
    190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    def __init__(self, path: str, piece_length: int, progress: bool = True):
    -    """
    -    Calculate and store hash information for specific file.
    -
    -    **DEPRECATED**
    -    """
    -    self.path = path
    -    self.root = None
    -    self.piece_layer = None
    -    self.layer_hashes = []
    -    self.piece_length = piece_length
    -    self.num_blocks = piece_length // BLOCK_SIZE
    -    if progress:
    -        self.prog_start(os.path.getsize(path), path, unit="bytes")
    -    with open(self.path, "rb") as fd:
    -        self.process_file(fd)
    -
    -
    -
    @@ -24814,40 +10292,6 @@

    Calculate root hash for the target file.

    DEPRECATED

    -
    - Source code in torrentfile\hasher.py -
    255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    def _calculate_root(self):
    -    """
    -    Calculate root hash for the target file.
    -
    -    **DEPRECATED**
    -    """
    -    self.piece_layer = b"".join(self.layer_hashes)
    -    hashes = len(self.layer_hashes)
    -    if hashes > 1:
    -        pow2 = next_power_2(hashes)
    -        remainder = pow2 - hashes
    -        pad_piece = [bytes(HASH_SIZE) for _ in range(self.num_blocks)]
    -        for _ in range(remainder):
    -            self.layer_hashes.append(merkle_root(pad_piece))
    -    self.root = merkle_root(self.layer_hashes)
    -
    -
    -
    @@ -24894,104 +10338,6 @@

    -
    - Source code in torrentfile\hasher.py -
    207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    def process_file(self, fd: str):
    -    """
    -    Calculate hashes over 16KiB chuncks of file content.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    fd : TextIOWrapper
    -        Opened file in read mode.
    -    """
    -    while True:
    -        blocks = []
    -        leaf = bytearray(BLOCK_SIZE)
    -        # generate leaves of merkle tree
    -
    -        for _ in range(self.num_blocks):
    -            size = fd.readinto(leaf)
    -            self.prog_update(size)
    -            if not size:
    -                break
    -            blocks.append(sha256(leaf[:size]).digest())
    -
    -        # blocks is empty mean eof
    -        if not blocks:
    -            break
    -        if len(blocks) != self.num_blocks:
    -            # when size of file doesn't fill the last block
    -            # when the file contains multiple pieces
    -            remaining = self.num_blocks - len(blocks)
    -            if not self.layer_hashes:
    -                # when the there is only one block for file
    -                power2 = next_power_2(len(blocks))
    -                remaining = power2 - len(blocks)
    -
    -            # pad the the rest with zeroes to fill remaining space.
    -            padding = [bytes(32) for _ in range(remaining)]
    -            self.prog_update(HASH_SIZE * remaining)
    -            blocks.extend(padding)
    -        # calculate the root hash for the merkle tree up to piece-length
    -
    -        layer_hash = merkle_root(blocks)
    -        if self._cb:
    -            self._cb(layer_hash)
    -        self.layer_hashes.append(layer_hash)
    -    self._calculate_root()
    -    self.prog_close()
    -
    -
    -
    @@ -25066,52 +10412,6 @@

    -
    - Source code in torrentfile\hasher.py -
    146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    def merkle_root(blocks: list) -> bytes:
    -    """
    -    Calculate the merkle root for a seq of sha256 hash digests.
    -
    -    Parameters
    -    ----------
    -    blocks : list
    -        a sequence of sha256 layer hashes.
    -
    -    Returns
    -    -------
    -    bytes
    -        the sha256 root hash of the merkle tree.
    -    """
    -    if blocks:
    -        while len(blocks) > 1:
    -            blocks = [
    -                sha256(x + y).digest() for x, y in zip(*[iter(blocks)] * 2)
    -            ]
    -        return blocks[0]
    -    return blocks
    -
    -
    -
    @@ -25130,15 +10430,7 @@

    - - -

    - torrentfile.interactive - - - -

    - +

    Module contains the procedures used for Interactive Mode.

    @@ -25171,190 +10463,6 @@

    Class namespace for interactive program options.

    -
    - Source code in torrentfile\interactive.py -
    293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    class InteractiveCreator:
    -    """
    -    Class namespace for interactive program options.
    -    """
    -
    -    def __init__(self):
    -        """
    -        Initialize interactive meta file creator dialog.
    -        """
    -        self.kwargs = {
    -            "announce": None,
    -            "url_list": None,
    -            "private": None,
    -            "source": None,
    -            "comment": None,
    -            "piece_length": None,
    -            "outfile": None,
    -            "path": None,
    -            "httpseeds": None,
    -        }
    -        self.outfile, self.meta = self.get_props()
    -
    -    def get_props(self):
    -        """
    -        Gather details for torrentfile from user.
    -        """
    -        piece_length = get_input(
    -            "Piece Length (empty=auto): ", lambda x: x.isdigit()
    -        )
    -
    -        self.kwargs["piece_length"] = piece_length
    -        announce = get_input(
    -            "Tracker list (empty): ", lambda x: isinstance(x, str)
    -        )
    -
    -        if announce:
    -            self.kwargs["announce"] = announce.split()
    -
    -        url_list = get_input(
    -            "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str)
    -        )
    -
    -        httpseeds = get_input(
    -            "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str)
    -        )
    -
    -        if url_list:
    -            self.kwargs["url_list"] = url_list.split()
    -        if httpseeds:
    -            self.kwargs["httpseeds"] = httpseeds.split()
    -        comment = get_input("Comment (empty): ", None)
    -
    -        if comment:
    -            self.kwargs["comment"] = comment
    -        source = get_input("Source (empty): ", None)
    -
    -        if source:
    -            self.kwargs["source"] = source
    -
    -        private = get_input(
    -            "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN"
    -        )
    -
    -        if private and private.lower() == "y":
    -            self.kwargs["private"] = 1
    -
    -        contents = get_input("Content Path: ", os.path.exists)
    -        self.kwargs["path"] = contents
    -
    -        outfile = get_input(
    -            f"Output Path ({contents}.torrent): ",
    -            lambda x: os.path.exists(os.path.dirname(x)),
    -        )
    -
    -        if outfile:
    -            self.kwargs["outfile"] = outfile
    -
    -        meta_version = get_input(
    -            "Meta Version {1,2,3}: (1)", lambda x: x in "123"
    -        )
    -
    -        showcenter(f"creating {outfile}")
    -
    -        if meta_version == "3":
    -            torrent = TorrentFileHybrid(**self.kwargs)
    -        elif meta_version == "2":
    -            torrent = TorrentFileV2(**self.kwargs)
    -        else:
    -            torrent = TorrentFile(**self.kwargs)
    -        return torrent.write()
    -
    -
    -
    @@ -25383,42 +10491,6 @@

    - Source code in torrentfile\interactive.py -
    298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    def __init__(self):
    -    """
    -    Initialize interactive meta file creator dialog.
    -    """
    -    self.kwargs = {
    -        "announce": None,
    -        "url_list": None,
    -        "private": None,
    -        "source": None,
    -        "comment": None,
    -        "piece_length": None,
    -        "outfile": None,
    -        "path": None,
    -        "httpseeds": None,
    -    }
    -    self.outfile, self.meta = self.get_props()
    -
    -
    -

    @@ -25440,146 +10512,6 @@

    - Source code in torrentfile\interactive.py -
    315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    def get_props(self):
    -    """
    -    Gather details for torrentfile from user.
    -    """
    -    piece_length = get_input(
    -        "Piece Length (empty=auto): ", lambda x: x.isdigit()
    -    )
    -
    -    self.kwargs["piece_length"] = piece_length
    -    announce = get_input(
    -        "Tracker list (empty): ", lambda x: isinstance(x, str)
    -    )
    -
    -    if announce:
    -        self.kwargs["announce"] = announce.split()
    -
    -    url_list = get_input(
    -        "Web Seed {GetRight} list (empty): ", lambda x: isinstance(x, str)
    -    )
    -
    -    httpseeds = get_input(
    -        "Web Seed {Hoffman} list (empty): ", lambda x: isinstance(x, str)
    -    )
    -
    -    if url_list:
    -        self.kwargs["url_list"] = url_list.split()
    -    if httpseeds:
    -        self.kwargs["httpseeds"] = httpseeds.split()
    -    comment = get_input("Comment (empty): ", None)
    -
    -    if comment:
    -        self.kwargs["comment"] = comment
    -    source = get_input("Source (empty): ", None)
    -
    -    if source:
    -        self.kwargs["source"] = source
    -
    -    private = get_input(
    -        "Private Torrent? {Y/N}: (N)", lambda x: x in "yYnN"
    -    )
    -
    -    if private and private.lower() == "y":
    -        self.kwargs["private"] = 1
    -
    -    contents = get_input("Content Path: ", os.path.exists)
    -    self.kwargs["path"] = contents
    -
    -    outfile = get_input(
    -        f"Output Path ({contents}.torrent): ",
    -        lambda x: os.path.exists(os.path.dirname(x)),
    -    )
    -
    -    if outfile:
    -        self.kwargs["outfile"] = outfile
    -
    -    meta_version = get_input(
    -        "Meta Version {1,2,3}: (1)", lambda x: x in "123"
    -    )
    -
    -    showcenter(f"creating {outfile}")
    -
    -    if meta_version == "3":
    -        torrent = TorrentFileHybrid(**self.kwargs)
    -    elif meta_version == "2":
    -        torrent = TorrentFileV2(**self.kwargs)
    -    else:
    -        torrent = TorrentFile(**self.kwargs)
    -    return torrent.write()
    -
    -
    - @@ -25614,212 +10546,6 @@

    Interactive dialog class for torrent editing.

    -
    - Source code in torrentfile\interactive.py -
    190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    class InteractiveEditor:
    -    """
    -    Interactive dialog class for torrent editing.
    -    """
    -
    -    def __init__(self, metafile: str):
    -        """
    -        Initialize the Interactive torrent editor guide.
    -
    -        Parameters
    -        ----------
    -        metafile : str
    -            user input string identifying the path to a torrent meta file.
    -        """
    -        self.metafile = metafile
    -        self.meta = pyben.load(metafile)
    -        self.info = self.meta["info"]
    -
    -        self.args = {
    -            "url-list": self.meta.get("url-list", None),
    -            "httpseeds": self.meta.get("httpseeds", None),
    -            "announce": self.meta.get("announce-list", None),
    -            "source": self.info.get("source", None),
    -            "private": self.info.get("private", None),
    -            "comment": self.info.get("comment", None),
    -        }
    -
    -    def show_current(self):
    -        """
    -        Display the current met file information to screen.
    -        """
    -        out = "Current properties and values:\n"
    -        longest = max([len(label) for label in self.args]) + 3
    -        for key, val in self.args.items():
    -            txt = (key.title() + ":").ljust(longest) + str(val)
    -            out += f"\t{txt}\n"
    -        showtext(out)
    -
    -    def sanatize_response(self, key, response):
    -        """
    -        Convert the input data into a form recognizable by the program.
    -
    -        Parameters
    -        ----------
    -        key : str
    -            name of the property and attribute being eddited.
    -        response : str
    -            User input value the property is being edited to.
    -        """
    -        if key in ["announce", "url-list", "httpseeds"]:
    -            val = response.split()
    -        else:
    -            val = response
    -        self.args[key] = val
    -
    -    def edit_props(self):
    -        """
    -        Loop continuosly for edits until user signals DONE.
    -        """
    -        while True:
    -            showcenter(
    -                "Choose the number for a propert the needs editing."
    -                "Enter DONE when all editing has been completed."
    -            )
    -
    -            props = {
    -                1: "comment",
    -                2: "source",
    -                3: "private",
    -                4: "tracker",
    -                5: "web-seed",
    -                6: "httpseeds",
    -            }
    -
    -            args = {
    -                1: "comment",
    -                2: "source",
    -                3: "private",
    -                4: "announce",
    -                5: "url-list",
    -                6: "httpseeds",
    -            }
    -
    -            txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
    -            prop = get_input(txt)
    -            if prop.lower() == "done":
    -                break
    -
    -            if prop.isdigit() and 0 < int(prop) < 6:
    -                key = props[int(prop)]
    -                key2 = args[int(prop)]
    -                val = self.args.get(key2)
    -                showtext(
    -                    "Enter new property value or leave empty for no value."
    -                )
    -                response = get_input(f"{key.title()} ({val}): ")
    -                self.sanatize_response(key2, response)
    -
    -            else:
    -                showtext("Invalid input: Try again.")
    -        edit_torrent(self.metafile, self.args)
    -
    -
    -
    @@ -25875,52 +10601,6 @@

    - Source code in torrentfile\interactive.py -
    195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    def __init__(self, metafile: str):
    -    """
    -    Initialize the Interactive torrent editor guide.
    -
    -    Parameters
    -    ----------
    -    metafile : str
    -        user input string identifying the path to a torrent meta file.
    -    """
    -    self.metafile = metafile
    -    self.meta = pyben.load(metafile)
    -    self.info = self.meta["info"]
    -
    -    self.args = {
    -        "url-list": self.meta.get("url-list", None),
    -        "httpseeds": self.meta.get("httpseeds", None),
    -        "announce": self.meta.get("announce-list", None),
    -        "source": self.info.get("source", None),
    -        "private": self.info.get("private", None),
    -        "comment": self.info.get("comment", None),
    -    }
    -
    -
    - @@ -25942,102 +10622,6 @@

    - Source code in torrentfile\interactive.py -
    245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    def edit_props(self):
    -    """
    -    Loop continuosly for edits until user signals DONE.
    -    """
    -    while True:
    -        showcenter(
    -            "Choose the number for a propert the needs editing."
    -            "Enter DONE when all editing has been completed."
    -        )
    -
    -        props = {
    -            1: "comment",
    -            2: "source",
    -            3: "private",
    -            4: "tracker",
    -            5: "web-seed",
    -            6: "httpseeds",
    -        }
    -
    -        args = {
    -            1: "comment",
    -            2: "source",
    -            3: "private",
    -            4: "announce",
    -            5: "url-list",
    -            6: "httpseeds",
    -        }
    -
    -        txt = ", ".join((str(k) + ": " + v) for k, v in props.items())
    -        prop = get_input(txt)
    -        if prop.lower() == "done":
    -            break
    -
    -        if prop.isdigit() and 0 < int(prop) < 6:
    -            key = props[int(prop)]
    -            key2 = args[int(prop)]
    -            val = self.args.get(key2)
    -            showtext(
    -                "Enter new property value or leave empty for no value."
    -            )
    -            response = get_input(f"{key.title()} ({val}): ")
    -            self.sanatize_response(key2, response)
    -
    -        else:
    -            showtext("Invalid input: Try again.")
    -    edit_torrent(self.metafile, self.args)
    -
    -
    - @@ -26093,42 +10677,6 @@

    - Source code in torrentfile\interactive.py -
    228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    def sanatize_response(self, key, response):
    -    """
    -    Convert the input data into a form recognizable by the program.
    -
    -    Parameters
    -    ----------
    -    key : str
    -        name of the property and attribute being eddited.
    -    response : str
    -        User input value the property is being edited to.
    -    """
    -    if key in ["announce", "url-list", "httpseeds"]:
    -        val = response.split()
    -    else:
    -        val = response
    -    self.args[key] = val
    -
    -
    - @@ -26150,30 +10698,6 @@

    - Source code in torrentfile\interactive.py -
    217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    def show_current(self):
    -    """
    -    Display the current met file information to screen.
    -    """
    -    out = "Current properties and values:\n"
    -    longest = max([len(label) for label in self.args]) + 3
    -    for key, val in self.args.items():
    -        txt = (key.title() + ":").ljust(longest) + str(val)
    -        out += f"\t{txt}\n"
    -    showtext(out)
    -
    -
    - @@ -26248,42 +10772,6 @@

    -
    - Source code in torrentfile\interactive.py -
    53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    def _get_input(txt: str):  # pragma: no cover
    -    """
    -    Gather information needed from user.
    -
    -    Parameters
    -    ----------
    -    txt : str
    -        The message usually containing instructions for the user.
    -
    -    Returns
    -    -------
    -    str
    -        The text input received from the user.
    -    """
    -    value = input(txt)
    -    return value
    -
    -
    -
    @@ -26357,56 +10845,6 @@

    -
    - Source code in torrentfile\interactive.py -
    71
    -72
    -73
    -74
    -75
    -76
    -77
    -78
    -79
    -80
    -81
    -82
    -83
    -84
    -85
    -86
    -87
    -88
    -89
    -90
    -91
    -92
    -93
    def _get_input_loop(txt, func):  # pragma: no cover
    -    """
    -    Gather information needed from user.
    -
    -    Parameters
    -    ----------
    -    txt : str
    -        The message usually containing instructions for the user.
    -    func : function
    -        Validate/Check user input data, failure = retry, success = continue.
    -
    -    Returns
    -    -------
    -    str
    -        The text input received from the user.
    -    """
    -    while True:
    -        value = input(txt)
    -        if func and func(value):
    -            return value
    -        if not func or value == "":
    -            return value
    -        showtext(f"Invalid input {value}: try again")
    -
    -
    -
    @@ -26428,38 +10866,6 @@

    Create new torrent file interactively.

    -
    - Source code in torrentfile\interactive.py -
    163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    def create_torrent():
    -    """
    -    Create new torrent file interactively.
    -    """
    -    showcenter("Create Torrent")
    -    showtext(
    -        "\nEnter values for each of the options for the torrent creator, "
    -        "or leave blank for program defaults.\nSpaces are considered item "
    -        "seperators for options that accept a list of values.\nValues "
    -        "enclosed in () indicate the default value, while {} holds all "
    -        "valid choices available for the option.\n\n"
    -    )
    -    creator = InteractiveCreator()
    -    return creator
    -
    -
    -
    @@ -26481,28 +10887,6 @@

    Edit the editable values of the torrent meta file.

    -
    - Source code in torrentfile\interactive.py -
    179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    def edit_action():
    -    """
    -    Edit the editable values of the torrent meta file.
    -    """
    -    showcenter("Edit Torrent")
    -    metafile = get_input("Metafile(.torrent): ", os.path.exists)
    -    dialog = InteractiveEditor(metafile)
    -    dialog.show_current()
    -    dialog.edit_props()
    -
    -
    -
    @@ -26566,44 +10950,6 @@

    -
    - Source code in torrentfile\interactive.py -
    34
    -35
    -36
    -37
    -38
    -39
    -40
    -41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    def get_input(*args: tuple):  # pragma: no cover
    -    """
    -    Determine appropriate input function to call.
    -
    -    Parameters
    -    ----------
    -    *args : tuple
    -        Arbitrary number of args to pass to next function
    -
    -    Returns
    -    -------
    -    str
    -        The results of the function call.
    -    """
    -    if len(args) == 2:
    -        return _get_input_loop(*args)
    -    return _get_input(*args)
    -
    -
    -
    @@ -26625,40 +10971,6 @@

    Check torrent download completed percentage.

    -
    - Source code in torrentfile\interactive.py -
    146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    def recheck_torrent():
    -    """
    -    Check torrent download completed percentage.
    -    """
    -    showcenter("Check Torrent")
    -    msg = "Enter path to torrent contents, and corresponding torrent metafile."
    -    showtext(msg)
    -    metafile = get_input(
    -        "Conent Path (downloads/complete/torrentname):", os.path.exists
    -    )
    -    contents = get_input("Metafile (*.torrent): ", os.path.exists)
    -    checker = Checker(metafile, contents)
    -    results = checker.results()
    -    showtext(f"Completion for {metafile} is {results}%")
    -    return results
    -
    -
    -
    @@ -26680,52 +10992,6 @@

    Operate TorrentFile program interactively through terminal.

    -
    - Source code in torrentfile\interactive.py -
    123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    def select_action():
    -    """
    -    Operate TorrentFile program interactively through terminal.
    -    """
    -    showcenter("TorrentFile: Starting Interactive Mode")
    -    action = get_input(
    -        "Enter the action you wish to perform.\n"
    -        "Action ( Create (c) | Edit (e) | Recheck (r) ): "
    -    )
    -    action = action.lower()
    -
    -    if "create" in action or action == "c":
    -        return create_torrent()
    -
    -    if "check" in action or action == "r":
    -        return recheck_torrent()
    -
    -    if "edit" in action or action == "e":
    -        return edit_action()
    -    print("Unable to recognize input.  Please try again.")  # pragma: nocover
    -    return select_action()  # pragma: nocover
    -
    -
    -
    @@ -26771,36 +11037,6 @@

    -
    - Source code in torrentfile\interactive.py -
    108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    def showcenter(txt: str):
    -    """
    -    Print text to screen in the center position of the terminal.
    -
    -    Parameters
    -    ----------
    -    txt : str
    -        the preformated message to send to stdout.
    -    """
    -    termlen = shutil.get_terminal_size().columns
    -    padding = " " * int(((termlen - len(txt)) / 2))
    -    string = "".join(["\n", padding, txt, "\n"])
    -    showtext(string)
    -
    -
    -
    @@ -26846,30 +11082,6 @@

    -
    - Source code in torrentfile\interactive.py -
     96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    def showtext(txt):
    -    """
    -    Print contents of txt to screen.
    -
    -    Parameters
    -    ----------
    -    txt : str
    -        text to print to terminal.
    -    """
    -    sys.stdout.write(txt)
    -
    -
    -
    @@ -26888,15 +11100,7 @@

    - - -

    - torrentfile.recheck - - - -

    - +

    Module container Checker Class.

    @@ -26982,554 +11186,6 @@
    Example
    -
    - Source code in torrentfile\recheck.py -
     48
    - 49
    - 50
    - 51
    - 52
    - 53
    - 54
    - 55
    - 56
    - 57
    - 58
    - 59
    - 60
    - 61
    - 62
    - 63
    - 64
    - 65
    - 66
    - 67
    - 68
    - 69
    - 70
    - 71
    - 72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    class Checker(ProgMixin):
    -    """
    -    Check a given file or directory to see if it matches a torrentfile.
    -
    -    Public constructor for Checker class instance.
    -
    -    Parameters
    -    ----------
    -    metafile : str
    -        Path to ".torrent" file.
    -    path : str
    -        Path where the content is located in filesystem.
    -
    -    Example
    -    -------
    -        >> metafile = "/path/to/torrentfile/content_file_or_dir.torrent"
    -        >> location = "/path/to/location"
    -        >> os.path.exists("/path/to/location/content_file_or_dir")
    -        Out: True
    -        >> checker = Checker(metafile, location)
    -    """
    -
    -    _hook = None
    -
    -    def __init__(self, metafile: str, path: str):
    -        """
    -        Validate data against hashes contained in .torrent file.
    -
    -        Parameters
    -        ----------
    -        metafile : str
    -            path to .torrent file
    -        path : str
    -            path to content or contents parent directory.
    -        """
    -        if not os.path.exists(metafile):
    -            raise FileNotFoundError
    -        meta = []
    -        thread = Thread(target=pyben.loadinto, args=(metafile, meta))
    -        thread.start()
    -        self.last_log = None
    -        self.log_msg("Checking: %s, %s", metafile, path)
    -        self.metafile = metafile
    -        self.total = 0
    -        self.paths = []
    -        self.fileinfo = {}
    -        thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
    -        if not meta:  # pragma: nocover
    -            thread2.start()
    -            thread2.join()
    -        self.meta = meta[0]
    -        self.info = self.meta["info"]
    -        self.name = self.info["name"]
    -        self.piece_length = self.info["piece length"]
    -
    -        if "meta version" in self.info:
    -            if "pieces" in self.info:
    -                self.meta_version = 3
    -            else:
    -                self.meta_version = 2
    -        else:
    -            self.meta_version = 1
    -
    -        self.root = self.find_root(path)
    -        self.check_paths()
    -
    -    @classmethod
    -    def register_callback(cls, hook):
    -        """
    -        Register hooks from 3rd party programs to access generated info.
    -
    -        Parameters
    -        ----------
    -        hook : function
    -            callback function for the logging feature.
    -        """
    -        cls._hook = hook
    -
    -    def piece_checker(self):
    -        """
    -        Check individual pieces of the torrent.
    -
    -        Returns
    -        -------
    -        HashChecker | FeedChecker
    -            Individual piece hasher.
    -        """
    -        if self.meta_version == 1:
    -            return FeedChecker
    -        return HashChecker
    -
    -    def results(self):
    -        """
    -        Generate result percentage and store for future calls.
    -        """
    -        responses = []
    -        for response in self.iter_hashes():
    -            responses.append(response)
    -
    -        self.log_msg(
    -            "Final result for %s recheck:  %s", self.metafile, self._result
    -        )
    -        return self._result
    -
    -    def log_msg(self, *args, level=logging.INFO):
    -        """
    -        Log message `msg` to logger and send `msg` to callback hook.
    -
    -        Parameters
    -        ----------
    -        *args : dict
    -            formatting args for log message
    -        level : int
    -            Log level for this message; default=`logging.INFO`
    -        """
    -        message = args[0]
    -        if len(args) >= 3:
    -            message = message % tuple(args[1:])
    -        elif len(args) == 2:
    -            message = message % args[1]
    -
    -        # Repeat log messages should be ignored.
    -        if message != self.last_log:
    -            self.last_log = message
    -            logger.log(level, message)
    -            if self._hook and level == logging.INFO:
    -                self._hook(message)
    -
    -    def find_root(self, path: str) -> str:
    -        """
    -        Check path for torrent content.
    -
    -        The path can be a relative or absolute filesystem path.  In the case
    -        where the content is a single file, the path may point directly to the
    -        the file, or it may point to the parent directory.  If content points
    -        to a directory.  The directory will be checked to see if it matches
    -        the torrent's name, if not the directories contents will be searched.
    -        The returned value will be the absolute path that matches the torrent's
    -        name.
    -
    -        Parameters
    -        ----------
    -        path : str
    -            root path to torrent content
    -
    -        Returns
    -        -------
    -        str
    -            root path to content
    -        """
    -        if not os.path.exists(path):
    -            self.log_msg("Could not locate torrent content %s.", path)
    -            raise FileNotFoundError(path)
    -
    -        root = Path(path)
    -        if root.name == self.name:
    -            self.log_msg("Content found: %s.", str(root))
    -            return root
    -
    -        if self.name in os.listdir(root):
    -            return root / self.name
    -
    -        self.log_msg("Could not locate torrent content in: %s", str(root))
    -        raise FileNotFoundError(root)
    -
    -    def check_paths(self):
    -        """
    -        Gather all file paths described in the torrent file.
    -        """
    -        finfo = self.fileinfo
    -
    -        if "length" in self.info:
    -            self.log_msg("%s points to a single file", self.root)
    -            self.total = self.info["length"]
    -            self.paths.append(str(self.root))
    -
    -            finfo[0] = {
    -                "path": self.root,
    -                "length": self.info["length"],
    -            }
    -
    -            if self.meta_version > 1:
    -                root = self.info["file tree"][self.name][""]["pieces root"]
    -                finfo[0]["pieces root"] = root
    -
    -            return
    -
    -        # Otherwise Content is more than 1 file.
    -        self.log_msg("%s points to a directory", self.root)
    -        if self.meta_version == 1:
    -
    -            for i, item in enumerate(self.info["files"]):
    -                self.total += item["length"]
    -                base = os.path.join(*item["path"])
    -
    -                self.fileinfo[i] = {
    -                    "path": str(self.root / base),
    -                    "length": item["length"],
    -                }
    -
    -                self.paths.append(str(self.root / base))
    -            return
    -
    -        self.walk_file_tree(self.info["file tree"], [])
    -
    -    def walk_file_tree(self, tree: dict, partials: list):
    -        """
    -        Traverse File Tree dictionary to get file details.
    -
    -        Extract full pathnames, length, root hash, and layer hashes
    -        for each file included in the .torrent's file tree.
    -
    -        Parameters
    -        ----------
    -        tree : dict
    -            File Tree dict extracted from torrent file.
    -        partials : list
    -            list of intermediate pathnames.
    -        """
    -        for key, val in tree.items():
    -
    -            # Empty string means the tree's leaf is value
    -            if "" in val:
    -
    -                base = os.path.join(*partials, key)
    -                roothash = None
    -                length = val[""]["length"]
    -                roothash = None if not length else val[""]["pieces root"]
    -                full = str(self.root / base)
    -                self.fileinfo[len(self.paths)] = {
    -                    "path": full,
    -                    "length": length,
    -                    "pieces root": roothash,
    -                }
    -                self.paths.append(full)
    -                self.total += length
    -            else:
    -                self.walk_file_tree(val, partials + [key])
    -
    -    def iter_hashes(self) -> tuple:
    -        """
    -        Produce results of comparing torrent contents piece by piece.
    -
    -        Yields
    -        ------
    -        chunck : bytes
    -            hash of data found on disk
    -        piece : bytes
    -            hash of data when complete and correct
    -        path : str
    -            path to file being hashed
    -        size : int
    -            length of bytes hashed for piece
    -        """
    -        matched = consumed = 0
    -        checker = self.piece_checker()
    -        for chunk, piece, path, size in checker(self):
    -            consumed += size
    -            matching = 0
    -            if chunk == piece:
    -                matching += size
    -                matched += size
    -            yield chunk, piece, path, size
    -            total_consumed = str(int(consumed / self.total * 100))
    -            percent_matched = str(int(matched / consumed * 100))
    -            self.log_msg(
    -                "Processed: %s%%, Matched: %s%%",
    -                total_consumed,
    -                percent_matched,
    -                level=logging.DEBUG,
    -            )
    -        self._result = (matched / consumed) * 100 if consumed > 0 else 0
    -
    -
    -
    @@ -27603,92 +11259,6 @@

    -
    - Source code in torrentfile\recheck.py -
     72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    def __init__(self, metafile: str, path: str):
    -    """
    -    Validate data against hashes contained in .torrent file.
    -
    -    Parameters
    -    ----------
    -    metafile : str
    -        path to .torrent file
    -    path : str
    -        path to content or contents parent directory.
    -    """
    -    if not os.path.exists(metafile):
    -        raise FileNotFoundError
    -    meta = []
    -    thread = Thread(target=pyben.loadinto, args=(metafile, meta))
    -    thread.start()
    -    self.last_log = None
    -    self.log_msg("Checking: %s, %s", metafile, path)
    -    self.metafile = metafile
    -    self.total = 0
    -    self.paths = []
    -    self.fileinfo = {}
    -    thread2 = Thread(target=waiting, args=("Extracting metadata", meta))
    -    if not meta:  # pragma: nocover
    -        thread2.start()
    -        thread2.join()
    -    self.meta = meta[0]
    -    self.info = self.meta["info"]
    -    self.name = self.info["name"]
    -    self.piece_length = self.info["piece length"]
    -
    -    if "meta version" in self.info:
    -        if "pieces" in self.info:
    -            self.meta_version = 3
    -        else:
    -            self.meta_version = 2
    -    else:
    -        self.meta_version = 1
    -
    -    self.root = self.find_root(path)
    -    self.check_paths()
    -
    -
    -

    @@ -27710,88 +11280,6 @@

    Gather all file paths described in the torrent file.

    -
    - Source code in torrentfile\recheck.py -
    213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    def check_paths(self):
    -    """
    -    Gather all file paths described in the torrent file.
    -    """
    -    finfo = self.fileinfo
    -
    -    if "length" in self.info:
    -        self.log_msg("%s points to a single file", self.root)
    -        self.total = self.info["length"]
    -        self.paths.append(str(self.root))
    -
    -        finfo[0] = {
    -            "path": self.root,
    -            "length": self.info["length"],
    -        }
    -
    -        if self.meta_version > 1:
    -            root = self.info["file tree"][self.name][""]["pieces root"]
    -            finfo[0]["pieces root"] = root
    -
    -        return
    -
    -    # Otherwise Content is more than 1 file.
    -    self.log_msg("%s points to a directory", self.root)
    -    if self.meta_version == 1:
    -
    -        for i, item in enumerate(self.info["files"]):
    -            self.total += item["length"]
    -            base = os.path.join(*item["path"])
    -
    -            self.fileinfo[i] = {
    -                "path": str(self.root / base),
    -                "length": item["length"],
    -            }
    -
    -            self.paths.append(str(self.root / base))
    -        return
    -
    -    self.walk_file_tree(self.info["file tree"], [])
    -
    -
    -
    @@ -27862,82 +11350,6 @@

    -
    - Source code in torrentfile\recheck.py -
    176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    def find_root(self, path: str) -> str:
    -    """
    -    Check path for torrent content.
    -
    -    The path can be a relative or absolute filesystem path.  In the case
    -    where the content is a single file, the path may point directly to the
    -    the file, or it may point to the parent directory.  If content points
    -    to a directory.  The directory will be checked to see if it matches
    -    the torrent's name, if not the directories contents will be searched.
    -    The returned value will be the absolute path that matches the torrent's
    -    name.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        root path to torrent content
    -
    -    Returns
    -    -------
    -    str
    -        root path to content
    -    """
    -    if not os.path.exists(path):
    -        self.log_msg("Could not locate torrent content %s.", path)
    -        raise FileNotFoundError(path)
    -
    -    root = Path(path)
    -    if root.name == self.name:
    -        self.log_msg("Content found: %s.", str(root))
    -        return root
    -
    -    if self.name in os.listdir(root):
    -        return root / self.name
    -
    -    self.log_msg("Could not locate torrent content in: %s", str(root))
    -    raise FileNotFoundError(root)
    -
    -
    -
    @@ -27995,76 +11407,6 @@

    -
    - Source code in torrentfile\recheck.py -
    287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    def iter_hashes(self) -> tuple:
    -    """
    -    Produce results of comparing torrent contents piece by piece.
    -
    -    Yields
    -    ------
    -    chunck : bytes
    -        hash of data found on disk
    -    piece : bytes
    -        hash of data when complete and correct
    -    path : str
    -        path to file being hashed
    -    size : int
    -        length of bytes hashed for piece
    -    """
    -    matched = consumed = 0
    -    checker = self.piece_checker()
    -    for chunk, piece, path, size in checker(self):
    -        consumed += size
    -        matching = 0
    -        if chunk == piece:
    -            matching += size
    -            matched += size
    -        yield chunk, piece, path, size
    -        total_consumed = str(int(consumed / self.total * 100))
    -        percent_matched = str(int(matched / consumed * 100))
    -        self.log_msg(
    -            "Processed: %s%%, Matched: %s%%",
    -            total_consumed,
    -            percent_matched,
    -            level=logging.DEBUG,
    -        )
    -    self._result = (matched / consumed) * 100 if consumed > 0 else 0
    -
    -
    -
    @@ -28120,56 +11462,6 @@

    -
    - Source code in torrentfile\recheck.py -
    152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    def log_msg(self, *args, level=logging.INFO):
    -    """
    -    Log message `msg` to logger and send `msg` to callback hook.
    -
    -    Parameters
    -    ----------
    -    *args : dict
    -        formatting args for log message
    -    level : int
    -        Log level for this message; default=`logging.INFO`
    -    """
    -    message = args[0]
    -    if len(args) >= 3:
    -        message = message % tuple(args[1:])
    -    elif len(args) == 2:
    -        message = message % args[1]
    -
    -    # Repeat log messages should be ignored.
    -    if message != self.last_log:
    -        self.last_log = message
    -        logger.log(level, message)
    -        if self._hook and level == logging.INFO:
    -            self._hook(message)
    -
    -
    -
    @@ -28209,34 +11501,6 @@

    -
    - Source code in torrentfile\recheck.py -
    126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    def piece_checker(self):
    -    """
    -    Check individual pieces of the torrent.
    -
    -    Returns
    -    -------
    -    HashChecker | FeedChecker
    -        Individual piece hasher.
    -    """
    -    if self.meta_version == 1:
    -        return FeedChecker
    -    return HashChecker
    -
    -
    -
    @@ -28285,32 +11549,6 @@

    -
    - Source code in torrentfile\recheck.py -
    114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    @classmethod
    -def register_callback(cls, hook):
    -    """
    -    Register hooks from 3rd party programs to access generated info.
    -
    -    Parameters
    -    ----------
    -    hook : function
    -        callback function for the logging feature.
    -    """
    -    cls._hook = hook
    -
    -
    -
    @@ -28332,34 +11570,6 @@

    Generate result percentage and store for future calls.

    -
    - Source code in torrentfile\recheck.py -
    139
    -140
    -141
    -142
    -143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    def results(self):
    -    """
    -    Generate result percentage and store for future calls.
    -    """
    -    responses = []
    -    for response in self.iter_hashes():
    -        responses.append(response)
    -
    -    self.log_msg(
    -        "Final result for %s recheck:  %s", self.metafile, self._result
    -    )
    -    return self._result
    -
    -
    -
    @@ -28417,76 +11627,6 @@

    -
    - Source code in torrentfile\recheck.py -
    253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    def walk_file_tree(self, tree: dict, partials: list):
    -    """
    -    Traverse File Tree dictionary to get file details.
    -
    -    Extract full pathnames, length, root hash, and layer hashes
    -    for each file included in the .torrent's file tree.
    -
    -    Parameters
    -    ----------
    -    tree : dict
    -        File Tree dict extracted from torrent file.
    -    partials : list
    -        list of intermediate pathnames.
    -    """
    -    for key, val in tree.items():
    -
    -        # Empty string means the tree's leaf is value
    -        if "" in val:
    -
    -            base = os.path.join(*partials, key)
    -            roothash = None
    -            length = val[""]["length"]
    -            roothash = None if not length else val[""]["pieces root"]
    -            full = str(self.root / base)
    -            self.fileinfo[len(self.paths)] = {
    -                "path": full,
    -                "length": length,
    -                "pieces root": roothash,
    -            }
    -            self.paths.append(full)
    -            self.total += length
    -        else:
    -            self.walk_file_tree(val, partials + [key])
    -
    -
    -
    @@ -28549,316 +11689,6 @@

    -
    - Source code in torrentfile\recheck.py -
    322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    -403
    -404
    -405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    class FeedChecker(ProgMixin):
    -    """
    -    Validates torrent content.
    -
    -    Seemlesly validate torrent file contents by comparing hashes in
    -    metafile against data on disk.
    -
    -    Parameters
    -    ----------
    -    checker : object
    -        the checker class instance.
    -    """
    -
    -    def __init__(self, checker: Checker):
    -        """
    -        Generate hashes of piece length data from filelist contents.
    -        """
    -        self.piece_length = checker.piece_length
    -        self.paths = checker.paths
    -        self.pieces = checker.info["pieces"]
    -        self.fileinfo = checker.fileinfo
    -        self.piece_map = {}
    -        self.index = 0
    -        self.piece_count = 0
    -        self.it = None
    -
    -    def __iter__(self):
    -        """
    -        Assign iterator and return self.
    -        """
    -        self.it = self.iter_pieces()
    -        return self
    -
    -    def __next__(self):
    -        """
    -        Yield back result of comparison.
    -        """
    -        try:
    -            partial = next(self.it)
    -        except StopIteration as itererror:
    -            raise StopIteration from itererror
    -
    -        chunck = sha1(partial).digest()  # nosec
    -        start = self.piece_count * SHA1
    -        end = start + SHA1
    -        piece = self.pieces[start:end]
    -        self.piece_count += 1
    -        path = self.paths[self.index]
    -        return chunck, piece, path, len(partial)
    -
    -    def iter_pieces(self):
    -        """
    -        Iterate through, and hash pieces of torrent contents.
    -
    -        Yields
    -        ------
    -        piece : bytes
    -            hash digest for block of torrent data.
    -        """
    -        partial = bytearray()
    -        for i, path in enumerate(self.paths):
    -            total = self.fileinfo[i]["length"]
    -            self.prog_start(total, path, unit="bytes")
    -            self.index = i
    -            if os.path.exists(path):
    -                for piece in self.extract(path, partial):
    -                    self.prog_update(len(piece))
    -                    if (len(piece) == self.piece_length) or (
    -                        i + 1 == len(self.paths)
    -                    ):
    -                        yield piece
    -                    else:
    -                        partial = piece
    -
    -            else:
    -                length = self.fileinfo[i]["length"]
    -                for pad in self._gen_padding(partial, length):
    -                    if len(pad) == self.piece_length:
    -                        yield pad
    -                    else:
    -                        partial = pad
    -            self.prog_close()
    -
    -    def extract(self, path: str, partial: bytearray) -> bytearray:
    -        """
    -        Split file paths contents into blocks of data for hash pieces.
    -
    -        Parameters
    -        ----------
    -        path : str
    -            path to content.
    -        partial : bytes
    -            any remaining content from last file.
    -
    -        Returns
    -        -------
    -        bytearray
    -            Hash digest for block of .torrent contents.
    -        """
    -        read = 0
    -        length = self.fileinfo[self.index]["length"]
    -        partial = bytearray() if len(partial) == self.piece_length else partial
    -        if path not in self.paths:  # pragma: no cover
    -            raise MissingPathError(path)
    -        with open(path, "rb") as current:
    -            while True:
    -                bitlength = self.piece_length - len(partial)
    -                part = bytearray(bitlength)
    -                amount = current.readinto(part)
    -                read += amount
    -                partial.extend(part[:amount])
    -                if amount < bitlength:
    -                    if amount > 0 and read == length:
    -                        self.prog_update(amount)
    -                        yield partial
    -                    break
    -                self.prog_update(amount)
    -                yield partial
    -                partial = bytearray(0)
    -        if length != read:
    -            for pad in self._gen_padding(partial, length, read):
    -                yield pad
    -
    -    def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
    -        """
    -        Create padded pieces where file sizes do not match.
    -
    -        Parameters
    -        ----------
    -        partial : bytes
    -            any remaining data from last file processed.
    -        length : int
    -            size of space that needs padding
    -        read : int
    -            portion of length already padded
    -
    -        Yields
    -        ------
    -        bytes
    -            A piece length sized block of zeros.
    -        """
    -        while read < length:
    -            left = self.piece_length - len(partial)
    -            if length - read > left:
    -                padding = bytearray(left)
    -                partial.extend(padding)
    -                yield partial
    -                read += left
    -                partial = bytearray(0)
    -            else:
    -                partial.extend(bytearray(length - read))
    -                read = length
    -                yield partial
    -
    -
    -
    @@ -28894,34 +11724,6 @@

    Generate hashes of piece length data from filelist contents.

    -
    - Source code in torrentfile\recheck.py -
    335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    def __init__(self, checker: Checker):
    -    """
    -    Generate hashes of piece length data from filelist contents.
    -    """
    -    self.piece_length = checker.piece_length
    -    self.paths = checker.paths
    -    self.pieces = checker.info["pieces"]
    -    self.fileinfo = checker.fileinfo
    -    self.piece_map = {}
    -    self.index = 0
    -    self.piece_count = 0
    -    self.it = None
    -
    -
    -
    @@ -28943,22 +11745,6 @@

    Assign iterator and return self.

    -
    - Source code in torrentfile\recheck.py -
    348
    -349
    -350
    -351
    -352
    -353
    def __iter__(self):
    -    """
    -    Assign iterator and return self.
    -    """
    -    self.it = self.iter_pieces()
    -    return self
    -
    -
    -
    @@ -28980,42 +11766,6 @@

    Yield back result of comparison.

    -
    - Source code in torrentfile\recheck.py -
    355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    def __next__(self):
    -    """
    -    Yield back result of comparison.
    -    """
    -    try:
    -        partial = next(self.it)
    -    except StopIteration as itererror:
    -        raise StopIteration from itererror
    -
    -    chunck = sha1(partial).digest()  # nosec
    -    start = self.piece_count * SHA1
    -    end = start + SHA1
    -    piece = self.pieces[start:end]
    -    self.piece_count += 1
    -    path = self.paths[self.index]
    -    return chunck, piece, path, len(partial)
    -
    -
    -
    @@ -29099,70 +11849,6 @@

    -
    - Source code in torrentfile\recheck.py -
    445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes:
    -    """
    -    Create padded pieces where file sizes do not match.
    -
    -    Parameters
    -    ----------
    -    partial : bytes
    -        any remaining data from last file processed.
    -    length : int
    -        size of space that needs padding
    -    read : int
    -        portion of length already padded
    -
    -    Yields
    -    ------
    -    bytes
    -        A piece length sized block of zeros.
    -    """
    -    while read < length:
    -        left = self.piece_length - len(partial)
    -        if length - read > left:
    -            padding = bytearray(left)
    -            partial.extend(padding)
    -            yield partial
    -            read += left
    -            partial = bytearray(0)
    -        else:
    -            partial.extend(bytearray(length - read))
    -            read = length
    -            yield partial
    -
    -
    -
    @@ -29236,88 +11922,6 @@

    -
    - Source code in torrentfile\recheck.py -
    405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    def extract(self, path: str, partial: bytearray) -> bytearray:
    -    """
    -    Split file paths contents into blocks of data for hash pieces.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        path to content.
    -    partial : bytes
    -        any remaining content from last file.
    -
    -    Returns
    -    -------
    -    bytearray
    -        Hash digest for block of .torrent contents.
    -    """
    -    read = 0
    -    length = self.fileinfo[self.index]["length"]
    -    partial = bytearray() if len(partial) == self.piece_length else partial
    -    if path not in self.paths:  # pragma: no cover
    -        raise MissingPathError(path)
    -    with open(path, "rb") as current:
    -        while True:
    -            bitlength = self.piece_length - len(partial)
    -            part = bytearray(bitlength)
    -            amount = current.readinto(part)
    -            read += amount
    -            partial.extend(part[:amount])
    -            if amount < bitlength:
    -                if amount > 0 and read == length:
    -                    self.prog_update(amount)
    -                    yield partial
    -                break
    -            self.prog_update(amount)
    -            yield partial
    -            partial = bytearray(0)
    -    if length != read:
    -        for pad in self._gen_padding(partial, length, read):
    -            yield pad
    -
    -
    -
    @@ -29357,74 +11961,6 @@

    -
    - Source code in torrentfile\recheck.py -
    372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    -403
    def iter_pieces(self):
    -    """
    -    Iterate through, and hash pieces of torrent contents.
    -
    -    Yields
    -    ------
    -    piece : bytes
    -        hash digest for block of torrent data.
    -    """
    -    partial = bytearray()
    -    for i, path in enumerate(self.paths):
    -        total = self.fileinfo[i]["length"]
    -        self.prog_start(total, path, unit="bytes")
    -        self.index = i
    -        if os.path.exists(path):
    -            for piece in self.extract(path, partial):
    -                self.prog_update(len(piece))
    -                if (len(piece) == self.piece_length) or (
    -                    i + 1 == len(self.paths)
    -                ):
    -                    yield piece
    -                else:
    -                    partial = piece
    -
    -        else:
    -            length = self.fileinfo[i]["length"]
    -            for pad in self._gen_padding(partial, length):
    -                if len(pad) == self.piece_length:
    -                    yield pad
    -                else:
    -                    partial = pad
    -        self.prog_close()
    -
    -
    -
    @@ -29458,7 +11994,7 @@

    Bases: ProgMixin

    -

    Verify that root hashes of content files match the .torrent files.

    +

    Iterate through contents of meta data and verify with file contents.

    Parameters:

    @@ -29474,7 +12010,7 @@

    checker - Object + Checker

    the checker instance that maintains variables.

    @@ -29485,364 +12021,6 @@

    -
    - Source code in torrentfile\recheck.py -
    477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    -544
    -545
    -546
    -547
    -548
    -549
    -550
    -551
    -552
    -553
    -554
    -555
    -556
    -557
    -558
    -559
    -560
    -561
    -562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    -572
    -573
    -574
    -575
    -576
    -577
    -578
    -579
    -580
    -581
    -582
    -583
    -584
    -585
    -586
    -587
    -588
    -589
    -590
    -591
    -592
    -593
    -594
    -595
    -596
    -597
    -598
    -599
    -600
    -601
    -602
    -603
    -604
    -605
    -606
    -607
    -608
    -609
    -610
    -611
    -612
    -613
    -614
    -615
    -616
    -617
    -618
    -619
    -620
    -621
    -622
    -623
    -624
    -625
    -626
    -627
    -628
    -629
    -630
    -631
    -632
    -633
    -634
    -635
    -636
    -637
    -638
    -639
    -640
    -641
    -642
    -643
    -644
    -645
    -646
    -647
    -648
    -649
    -650
    -651
    -652
    -653
    class HashChecker(ProgMixin):
    -    """
    -    Verify that root hashes of content files match the .torrent files.
    -
    -    Parameters
    -    ----------
    -    checker : Object
    -        the checker instance that maintains variables.
    -    """
    -
    -    def __init__(self, checker: Checker):
    -        """
    -        Construct a HybridChecker instance.
    -        """
    -        self.checker = checker
    -        self.paths = checker.paths
    -        self.piece_length = checker.piece_length
    -        self.fileinfo = checker.fileinfo
    -        self.piece_layers = checker.meta["piece layers"]
    -        self.current = None
    -        self.index = -1
    -
    -    def __iter__(self):
    -        """
    -        Assign iterator and return self.
    -        """
    -        return self
    -
    -    def __next__(self):
    -        """
    -        Provide the result of comparison.
    -        """
    -        if self.current is None:
    -            self.next_file()
    -        try:
    -            return self.process_current()
    -        except StopIteration as itererr:
    -            if self.next_file():
    -                return self.process_current()
    -            raise StopIteration from itererr
    -
    -    class Padder:
    -        """
    -        Padding class to generate padding hashes wherever needed.
    -
    -        Parameters
    -        ----------
    -        length: int
    -            the total size of the mock file generating padding for.
    -        piece_length : int
    -            the block size that each hash represents.
    -        """
    -
    -        def __init__(self, length, piece_length):
    -            """
    -            Construct padding class to Mock missing or incomplete files.
    -
    -            Parameters
    -            ----------
    -            length : int
    -                size of the file
    -            piece_length : int
    -                the piece length for each iteration.
    -            """
    -            self.length = length
    -            self.piece_length = piece_length
    -            self.pad = sha256(bytearray(piece_length)).digest()
    -
    -        def __iter__(self):
    -            """
    -            Return self to correctly implement iterator type.
    -            """
    -            return self  # pragma: nocover
    -
    -        def __next__(self) -> bytes:
    -            """
    -            Iterate through seemingly endless sha256 hashes of zeros.
    -
    -            Returns
    -            -------
    -            tuple :
    -                returns the padding
    -
    -            Raises
    -            ------
    -            StopIteration
    -            """
    -            if self.length >= self.piece_length:
    -                self.length -= self.piece_length
    -                return self.pad
    -            if self.length > 0:
    -                pad = sha256(bytearray(self.length)).digest()
    -                self.length -= self.length
    -                return pad
    -            raise StopIteration
    -
    -    def next_file(self) -> bool:
    -        """
    -        Remove all references to  processed files and prepare for the next.
    -
    -        Returns
    -        -------
    -        bool
    -            if there is a next file found
    -        """
    -        self.index += 1
    -        self.prog_close()
    -        if self.current is None or self.index < len(self.paths):
    -            self.current = self.paths[self.index]
    -            self.length = self.fileinfo[self.index]["length"]
    -            self.root_hash = self.fileinfo[self.index]["pieces root"]
    -            if self.length > self.piece_length:
    -                self.pieces = self.piece_layers[self.root_hash]
    -            else:
    -                self.pieces = self.root_hash
    -            path = self.paths[self.index]
    -            self.prog_start(self.length, path, unit="bytes")
    -            self.count = 0
    -            if os.path.exists(self.current):
    -                self.hasher = FileHasher(path, self.piece_length, progress=0)
    -            else:
    -                self.hasher = self.Padder(self.length, self.piece_length)
    -            return True
    -        if self.index >= len(self.paths):
    -            del self.current
    -            del self.length
    -            del self.root_hash
    -            del self.pieces
    -        return False
    -
    -    def process_current(self) -> tuple:
    -        """
    -        Gather necessary information to compare to metafile details.
    -
    -        Returns
    -        -------
    -        tuple
    -            a tuple containing the layer, piece, current path and size
    -
    -        Raises
    -        ------
    -        StopIteration
    -        """
    -        try:
    -            layer = next(self.hasher)
    -            piece, size = self.advance()
    -            self.prog_update(size)
    -            return layer, piece, self.current, size
    -        except StopIteration as err:
    -            if self.length > 0 and self.count * SHA256 < len(self.pieces):
    -                self.hasher = self.Padder(self.length, self.piece_length)
    -                piece, size = self.advance()
    -                layer = next(self.hasher)
    -                self.prog_update(0)
    -                return layer, piece, self.current, size
    -            raise StopIteration from err
    -
    -    def advance(self) -> tuple:
    -        """
    -        Increment the number of pieces processed for the current file.
    -
    -        Returns
    -        -------
    -        tuple
    -            the piece and size
    -        """
    -        start = self.count * SHA256
    -        end = start + SHA256
    -        piece = self.pieces[start:end]
    -        self.count += 1
    -        if self.length >= self.piece_length:
    -            self.length -= self.piece_length
    -            size = self.piece_length
    -        else:
    -            size = self.length
    -            self.length -= self.length
    -        return piece, size
    -
    -
    -
    @@ -29912,118 +12090,6 @@

    -
    - Source code in torrentfile\recheck.py -
    518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    -544
    -545
    -546
    -547
    -548
    -549
    -550
    -551
    -552
    -553
    -554
    -555
    -556
    -557
    -558
    -559
    -560
    -561
    -562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    class Padder:
    -    """
    -    Padding class to generate padding hashes wherever needed.
    -
    -    Parameters
    -    ----------
    -    length: int
    -        the total size of the mock file generating padding for.
    -    piece_length : int
    -        the block size that each hash represents.
    -    """
    -
    -    def __init__(self, length, piece_length):
    -        """
    -        Construct padding class to Mock missing or incomplete files.
    -
    -        Parameters
    -        ----------
    -        length : int
    -            size of the file
    -        piece_length : int
    -            the piece length for each iteration.
    -        """
    -        self.length = length
    -        self.piece_length = piece_length
    -        self.pad = sha256(bytearray(piece_length)).digest()
    -
    -    def __iter__(self):
    -        """
    -        Return self to correctly implement iterator type.
    -        """
    -        return self  # pragma: nocover
    -
    -    def __next__(self) -> bytes:
    -        """
    -        Iterate through seemingly endless sha256 hashes of zeros.
    -
    -        Returns
    -        -------
    -        tuple :
    -            returns the padding
    -
    -        Raises
    -        ------
    -        StopIteration
    -        """
    -        if self.length >= self.piece_length:
    -            self.length -= self.piece_length
    -            return self.pad
    -        if self.length > 0:
    -            pad = sha256(bytearray(self.length)).digest()
    -            self.length -= self.length
    -            return pad
    -        raise StopIteration
    -
    -
    -
    @@ -30088,38 +12154,6 @@

    -
    - Source code in torrentfile\recheck.py -
    530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    def __init__(self, length, piece_length):
    -    """
    -    Construct padding class to Mock missing or incomplete files.
    -
    -    Parameters
    -    ----------
    -    length : int
    -        size of the file
    -    piece_length : int
    -        the piece length for each iteration.
    -    """
    -    self.length = length
    -    self.piece_length = piece_length
    -    self.pad = sha256(bytearray(piece_length)).digest()
    -
    -
    -
    @@ -30141,20 +12175,6 @@
    Return self to correctly implement iterator type.

    -
    - Source code in torrentfile\recheck.py -
    545
    -546
    -547
    -548
    -549
    def __iter__(self):
    -    """
    -    Return self to correctly implement iterator type.
    -    """
    -    return self  # pragma: nocover
    -
    -
    -
    @@ -30212,52 +12232,6 @@
    -
    - Source code in torrentfile\recheck.py -
    551
    -552
    -553
    -554
    -555
    -556
    -557
    -558
    -559
    -560
    -561
    -562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    def __next__(self) -> bytes:
    -    """
    -    Iterate through seemingly endless sha256 hashes of zeros.
    -
    -    Returns
    -    -------
    -    tuple :
    -        returns the padding
    -
    -    Raises
    -    ------
    -    StopIteration
    -    """
    -    if self.length >= self.piece_length:
    -        self.length -= self.piece_length
    -        return self.pad
    -    if self.length > 0:
    -        pad = sha256(bytearray(self.length)).digest()
    -        self.length -= self.length
    -        return pad
    -    raise StopIteration
    -
    -
    -
    @@ -30290,32 +12264,6 @@

    Construct a HybridChecker instance.

    -
    - Source code in torrentfile\recheck.py -
    487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    def __init__(self, checker: Checker):
    -    """
    -    Construct a HybridChecker instance.
    -    """
    -    self.checker = checker
    -    self.paths = checker.paths
    -    self.piece_length = checker.piece_length
    -    self.fileinfo = checker.fileinfo
    -    self.piece_layers = checker.meta["piece layers"]
    -    self.current = None
    -    self.index = -1
    -
    -
    -
    @@ -30337,20 +12285,6 @@

    Assign iterator and return self.

    -
    - Source code in torrentfile\recheck.py -
    499
    -500
    -501
    -502
    -503
    def __iter__(self):
    -    """
    -    Assign iterator and return self.
    -    """
    -    return self
    -
    -
    -
    @@ -30372,34 +12306,6 @@

    Provide the result of comparison.

    -
    - Source code in torrentfile\recheck.py -
    505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    def __next__(self):
    -    """
    -    Provide the result of comparison.
    -    """
    -    if self.current is None:
    -        self.next_file()
    -    try:
    -        return self.process_current()
    -    except StopIteration as itererr:
    -        if self.next_file():
    -            return self.process_current()
    -        raise StopIteration from itererr
    -
    -
    -
    @@ -30439,50 +12345,6 @@

    -
    - Source code in torrentfile\recheck.py -
    634
    -635
    -636
    -637
    -638
    -639
    -640
    -641
    -642
    -643
    -644
    -645
    -646
    -647
    -648
    -649
    -650
    -651
    -652
    -653
    def advance(self) -> tuple:
    -    """
    -    Increment the number of pieces processed for the current file.
    -
    -    Returns
    -    -------
    -    tuple
    -        the piece and size
    -    """
    -    start = self.count * SHA256
    -    end = start + SHA256
    -    piece = self.pieces[start:end]
    -    self.count += 1
    -    if self.length >= self.piece_length:
    -        self.length -= self.piece_length
    -        size = self.piece_length
    -    else:
    -        size = self.length
    -        self.length -= self.length
    -    return piece, size
    -
    -
    -
    @@ -30522,76 +12384,6 @@

    -
    - Source code in torrentfile\recheck.py -
    573
    -574
    -575
    -576
    -577
    -578
    -579
    -580
    -581
    -582
    -583
    -584
    -585
    -586
    -587
    -588
    -589
    -590
    -591
    -592
    -593
    -594
    -595
    -596
    -597
    -598
    -599
    -600
    -601
    -602
    -603
    -604
    -605
    def next_file(self) -> bool:
    -    """
    -    Remove all references to  processed files and prepare for the next.
    -
    -    Returns
    -    -------
    -    bool
    -        if there is a next file found
    -    """
    -    self.index += 1
    -    self.prog_close()
    -    if self.current is None or self.index < len(self.paths):
    -        self.current = self.paths[self.index]
    -        self.length = self.fileinfo[self.index]["length"]
    -        self.root_hash = self.fileinfo[self.index]["pieces root"]
    -        if self.length > self.piece_length:
    -            self.pieces = self.piece_layers[self.root_hash]
    -        else:
    -            self.pieces = self.root_hash
    -        path = self.paths[self.index]
    -        self.prog_start(self.length, path, unit="bytes")
    -        self.count = 0
    -        if os.path.exists(self.current):
    -            self.hasher = FileHasher(path, self.piece_length, progress=0)
    -        else:
    -            self.hasher = self.Padder(self.length, self.piece_length)
    -        return True
    -    if self.index >= len(self.paths):
    -        del self.current
    -        del self.length
    -        del self.root_hash
    -        del self.pieces
    -    return False
    -
    -
    -
    @@ -30649,62 +12441,6 @@

    -
    - Source code in torrentfile\recheck.py -
    607
    -608
    -609
    -610
    -611
    -612
    -613
    -614
    -615
    -616
    -617
    -618
    -619
    -620
    -621
    -622
    -623
    -624
    -625
    -626
    -627
    -628
    -629
    -630
    -631
    -632
    def process_current(self) -> tuple:
    -    """
    -    Gather necessary information to compare to metafile details.
    -
    -    Returns
    -    -------
    -    tuple
    -        a tuple containing the layer, piece, current path and size
    -
    -    Raises
    -    ------
    -    StopIteration
    -    """
    -    try:
    -        layer = next(self.hasher)
    -        piece, size = self.advance()
    -        self.prog_update(size)
    -        return layer, piece, self.current, size
    -    except StopIteration as err:
    -        if self.length > 0 and self.count * SHA256 < len(self.pieces):
    -            self.hasher = self.Padder(self.length, self.piece_length)
    -            piece, size = self.advance()
    -            layer = next(self.hasher)
    -            self.prog_update(0)
    -            return layer, piece, self.current, size
    -        raise StopIteration from err
    -
    -
    -
    @@ -30734,15 +12470,7 @@

    - - -

    - torrentfile.torrent - - - -

    - +

    Classes and procedures pertaining to the creation of torrent meta files.

    @@ -31116,408 +12844,6 @@

    -
    - Source code in torrentfile\torrent.py -
    204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    -362
    -363
    -364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    class MetaFile:
    -    """
    -    Base Class for all TorrentFile classes.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        target path to torrent content.  Default: None
    -    announce : str
    -        One or more tracker URL's.  Default: None
    -    comment : str
    -        A comment.  Default: None
    -    piece_length : int
    -        Size of torrent pieces.  Default: None
    -    private : bool
    -        For private trackers.  Default: None
    -    outfile : str
    -        target path to write .torrent file. Default: None
    -    source : str
    -        Private tracker source. Default: None
    -    progress : str
    -        level of progress bar displayed  Default: "1"
    -    cwd : bool
    -        If True change default save location to current directory
    -    httpseeds : list
    -        one or more web addresses where torrent content can be found.
    -    url_list : list
    -        one or more web addressess where torrent content exists.
    -    content : str
    -        alias for 'path' arg.
    -    meta_version : int
    -        indicates which Bittorrent protocol to use for hashing content
    -    """
    -
    -    hasher = None
    -
    -    @classmethod
    -    def set_callback(cls, func):
    -        """
    -        Assign a callback function for the Hashing class to call for each hash.
    -
    -        Parameters
    -        ----------
    -        func : function
    -            The callback function which accepts a single paramter.
    -        """
    -        if "hasher" in vars(cls) and vars(cls)["hasher"]:
    -            cls.hasher.set_callback(func)
    -
    -    def __init__(
    -        self,
    -        path=None,
    -        announce=None,
    -        comment=None,
    -        piece_length=None,
    -        private=False,
    -        outfile=None,
    -        source=None,
    -        progress=1,
    -        cwd=False,
    -        httpseeds=None,
    -        url_list=None,
    -        content=None,
    -        meta_version=None,
    -        **_,
    -    ):
    -        """
    -        Construct MetaFile superclass and assign local attributes.
    -        """
    -        self.private = private
    -        self.cwd = cwd
    -        self.outfile = outfile
    -        self.progress = int(progress)
    -        self.comment = comment
    -        self.source = source
    -        self.meta_version = meta_version
    -
    -        if content:
    -            path = content
    -        if not path:
    -            if announce and len(announce) > 1 and os.path.exists(announce[-1]):
    -                path = announce[-1]
    -                announce = announce[:-1]
    -            elif url_list and os.path.exists(url_list[-1]):
    -                path = url_list[-1]
    -                url_list = url_list[:-1]
    -            elif httpseeds and os.path.exists(httpseeds[-1]):
    -                path = httpseeds[-1]
    -                httpseeds = httpseeds[:-1]
    -            else:
    -                raise utils.MissingPathError("Path to content is required.")
    -
    -        # base path to torrent content.
    -        self.path = path
    -
    -        logger.debug("path parameter found %s", path)
    -
    -        # Format piece_length attribute.
    -        if piece_length:
    -            self.piece_length = utils.normalize_piece_length(piece_length)
    -            logger.debug("piece length parameter found %s", piece_length)
    -        else:
    -            self.piece_length = utils.path_piece_length(self.path)
    -            logger.debug("piece length calculated %s", self.piece_length)
    -
    -        # Assign announce URL to empty string if none provided.
    -        if not announce:
    -            self.announce, self.announce_list = "", [[""]]
    -
    -        # Most torrent clients have editting trackers as a feature.
    -        elif isinstance(announce, str):
    -            self.announce, self.announce_list = announce, [[announce]]
    -
    -        elif isinstance(announce, Sequence):
    -            self.announce, self.announce_list = announce[0], [announce]
    -
    -        self.meta = {
    -            "announce": self.announce,
    -            "announce-list": self.announce_list,
    -            "created by": f"TorrentFile:v{version}",
    -            "creation date": int(datetime.timestamp(datetime.now())),
    -            "info": {},
    -        }
    -        if comment:
    -            self.meta["info"]["comment"] = comment
    -            logger.debug("comment parameter found %s", comment)
    -        if private:
    -            self.meta["info"]["private"] = 1
    -            logger.debug("private parameter triggered")
    -        if source:
    -            self.meta["info"]["source"] = source
    -            logger.debug("source parameter found %s", source)
    -        if url_list:
    -            self.meta["url-list"] = url_list
    -            logger.debug("url list parameter found %s", str(url_list))
    -        if httpseeds:
    -            self.meta["httpseeds"] = httpseeds
    -            logger.debug("httpseeds parameter found %s", str(httpseeds))
    -        self.meta["info"]["piece length"] = self.piece_length
    -
    -        self.meta_version = meta_version
    -        parent, self.name = os.path.split(self.path)
    -        if not self.name:
    -            self.name = os.path.basename(parent)
    -        self.meta["info"]["name"] = self.name
    -
    -    def assemble(self):
    -        """
    -        Overload in subclasses.
    -
    -        Raises
    -        ------
    -        Exception
    -            NotImplementedError
    -        """
    -        raise NotImplementedError
    -
    -    def sort_meta(self):
    -        """Sort the info and meta dictionaries."""
    -        logger.debug("sorting dictionary keys")
    -        meta = self.meta
    -        meta["info"] = dict(sorted(list(meta["info"].items())))
    -        meta = dict(sorted(list(meta.items())))
    -        return meta
    -
    -    def write(self, outfile=None) -> tuple:
    -        """
    -        Write meta information to .torrent file.
    -
    -        Parameters
    -        ----------
    -        outfile : str
    -            Destination path for .torrent file. default=None
    -
    -        Returns
    -        -------
    -        outfile : str
    -            Where the .torrent file was writen.
    -        meta : dict
    -            .torrent meta information.
    -        """
    -        fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
    -        if not self.outfile and not outfile:
    -            if self.cwd:
    -                self.outfile = fallback
    -            else:
    -                path = str(self.path).rstrip("\\/")
    -                self.outfile = path + ".torrent"
    -        elif outfile:
    -            self.outfile = outfile
    -        if str(self.outfile)[-1] in "\\/":
    -            self.outfile = self.outfile + (self.name + ".torrent")
    -        self.meta = self.sort_meta()
    -        try:
    -            pyben.dump(self.meta, self.outfile)
    -        except PermissionError:
    -            self.outfile = fallback
    -            pyben.dump(self.meta, fallback)
    -        return self.outfile, self.meta
    -
    -
    -
    @@ -31557,202 +12883,6 @@

    Construct MetaFile superclass and assign local attributes.

    -
    - Source code in torrentfile\torrent.py -
    253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    def __init__(
    -    self,
    -    path=None,
    -    announce=None,
    -    comment=None,
    -    piece_length=None,
    -    private=False,
    -    outfile=None,
    -    source=None,
    -    progress=1,
    -    cwd=False,
    -    httpseeds=None,
    -    url_list=None,
    -    content=None,
    -    meta_version=None,
    -    **_,
    -):
    -    """
    -    Construct MetaFile superclass and assign local attributes.
    -    """
    -    self.private = private
    -    self.cwd = cwd
    -    self.outfile = outfile
    -    self.progress = int(progress)
    -    self.comment = comment
    -    self.source = source
    -    self.meta_version = meta_version
    -
    -    if content:
    -        path = content
    -    if not path:
    -        if announce and len(announce) > 1 and os.path.exists(announce[-1]):
    -            path = announce[-1]
    -            announce = announce[:-1]
    -        elif url_list and os.path.exists(url_list[-1]):
    -            path = url_list[-1]
    -            url_list = url_list[:-1]
    -        elif httpseeds and os.path.exists(httpseeds[-1]):
    -            path = httpseeds[-1]
    -            httpseeds = httpseeds[:-1]
    -        else:
    -            raise utils.MissingPathError("Path to content is required.")
    -
    -    # base path to torrent content.
    -    self.path = path
    -
    -    logger.debug("path parameter found %s", path)
    -
    -    # Format piece_length attribute.
    -    if piece_length:
    -        self.piece_length = utils.normalize_piece_length(piece_length)
    -        logger.debug("piece length parameter found %s", piece_length)
    -    else:
    -        self.piece_length = utils.path_piece_length(self.path)
    -        logger.debug("piece length calculated %s", self.piece_length)
    -
    -    # Assign announce URL to empty string if none provided.
    -    if not announce:
    -        self.announce, self.announce_list = "", [[""]]
    -
    -    # Most torrent clients have editting trackers as a feature.
    -    elif isinstance(announce, str):
    -        self.announce, self.announce_list = announce, [[announce]]
    -
    -    elif isinstance(announce, Sequence):
    -        self.announce, self.announce_list = announce[0], [announce]
    -
    -    self.meta = {
    -        "announce": self.announce,
    -        "announce-list": self.announce_list,
    -        "created by": f"TorrentFile:v{version}",
    -        "creation date": int(datetime.timestamp(datetime.now())),
    -        "info": {},
    -    }
    -    if comment:
    -        self.meta["info"]["comment"] = comment
    -        logger.debug("comment parameter found %s", comment)
    -    if private:
    -        self.meta["info"]["private"] = 1
    -        logger.debug("private parameter triggered")
    -    if source:
    -        self.meta["info"]["source"] = source
    -        logger.debug("source parameter found %s", source)
    -    if url_list:
    -        self.meta["url-list"] = url_list
    -        logger.debug("url list parameter found %s", str(url_list))
    -    if httpseeds:
    -        self.meta["httpseeds"] = httpseeds
    -        logger.debug("httpseeds parameter found %s", str(httpseeds))
    -    self.meta["info"]["piece length"] = self.piece_length
    -
    -    self.meta_version = meta_version
    -    parent, self.name = os.path.split(self.path)
    -    if not self.name:
    -        self.name = os.path.basename(parent)
    -    self.meta["info"]["name"] = self.name
    -
    -
    -

    @@ -31792,30 +12922,6 @@

    -
    - Source code in torrentfile\torrent.py -
    350
    -351
    -352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    def assemble(self):
    -    """
    -    Overload in subclasses.
    -
    -    Raises
    -    ------
    -    Exception
    -        NotImplementedError
    -    """
    -    raise NotImplementedError
    -
    -
    -
    @@ -31864,34 +12970,6 @@

    -
    - Source code in torrentfile\torrent.py -
    240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    @classmethod
    -def set_callback(cls, func):
    -    """
    -    Assign a callback function for the Hashing class to call for each hash.
    -
    -    Parameters
    -    ----------
    -    func : function
    -        The callback function which accepts a single paramter.
    -    """
    -    if "hasher" in vars(cls) and vars(cls)["hasher"]:
    -        cls.hasher.set_callback(func)
    -
    -
    -
    @@ -31913,24 +12991,6 @@

    Sort the info and meta dictionaries.

    -
    - Source code in torrentfile\torrent.py -
    361
    -362
    -363
    -364
    -365
    -366
    -367
    def sort_meta(self):
    -    """Sort the info and meta dictionaries."""
    -    logger.debug("sorting dictionary keys")
    -    meta = self.meta
    -    meta["info"] = dict(sorted(list(meta["info"].items())))
    -    meta = dict(sorted(list(meta.items())))
    -    return meta
    -
    -
    -
    @@ -32000,78 +13060,6 @@

    -
    - Source code in torrentfile\torrent.py -
    369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    -402
    def write(self, outfile=None) -> tuple:
    -    """
    -    Write meta information to .torrent file.
    -
    -    Parameters
    -    ----------
    -    outfile : str
    -        Destination path for .torrent file. default=None
    -
    -    Returns
    -    -------
    -    outfile : str
    -        Where the .torrent file was writen.
    -    meta : dict
    -        .torrent meta information.
    -    """
    -    fallback = os.path.join(os.getcwd(), self.name) + ".torrent"
    -    if not self.outfile and not outfile:
    -        if self.cwd:
    -            self.outfile = fallback
    -        else:
    -            path = str(self.path).rstrip("\\/")
    -            self.outfile = path + ".torrent"
    -    elif outfile:
    -        self.outfile = outfile
    -    if str(self.outfile)[-1] in "\\/":
    -        self.outfile = self.outfile + (self.name + ".torrent")
    -    self.meta = self.sort_meta()
    -    try:
    -        pyben.dump(self.meta, self.outfile)
    -    except PermissionError:
    -        self.outfile = fallback
    -        pyben.dump(self.meta, fallback)
    -    return self.outfile, self.meta
    -
    -
    -
    @@ -32134,204 +13122,6 @@

    -
    - Source code in torrentfile\torrent.py -
    643
    -644
    -645
    -646
    -647
    -648
    -649
    -650
    -651
    -652
    -653
    -654
    -655
    -656
    -657
    -658
    -659
    -660
    -661
    -662
    -663
    -664
    -665
    -666
    -667
    -668
    -669
    -670
    -671
    -672
    -673
    -674
    -675
    -676
    -677
    -678
    -679
    -680
    -681
    -682
    -683
    -684
    -685
    -686
    -687
    -688
    -689
    -690
    -691
    -692
    -693
    -694
    -695
    -696
    -697
    -698
    -699
    -700
    -701
    -702
    -703
    -704
    -705
    -706
    -707
    -708
    -709
    -710
    -711
    -712
    -713
    -714
    -715
    -716
    -717
    -718
    -719
    -720
    -721
    -722
    -723
    -724
    -725
    -726
    -727
    -728
    -729
    -730
    -731
    -732
    -733
    -734
    -735
    -736
    -737
    -738
    -739
    class TorrentAssembler(MetaFile):
    -    """
    -    Assembler class for Bittorrent version 2 and hybrid meta files.
    -
    -    This differs from the TorrentFileV2 and TorrentFileHybrid, because
    -    it can be used as an iterator and works for both versions.
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        Keyword arguments for torrent options.
    -    """
    -
    -    hasher = FileHasher
    -
    -    def __init__(self, **kwargs):
    -        """
    -        Create Bittorrent v1 v2 hybrid metafiles.
    -        """
    -        super().__init__(**kwargs)
    -        logger.debug("Assembling bittorrent Hybrid file")
    -        self.name = os.path.basename(self.path)
    -        self.hashes = []
    -        self.piece_layers = {}
    -        self.pieces = bytearray()
    -        self.files = []
    -        self.hybrid = self.meta_version == "3"
    -        self.total = len(utils.get_file_list(self.path))
    -        self.assemble()
    -
    -    def assemble(self):
    -        """
    -        Assemble the parts of the torrentfile into meta dictionary.
    -        """
    -        info = self.meta["info"]
    -        info["meta version"] = 2
    -
    -        if os.path.isfile(self.path):
    -            info["file tree"] = {self.name: self._traverse(self.path)}
    -            info["length"] = os.path.getsize(self.path)
    -
    -        else:
    -            info["file tree"] = self._traverse(self.path)
    -            if self.hybrid:
    -                info["files"] = self.files
    -
    -        if self.hybrid:
    -            info["pieces"] = self.pieces
    -        self.meta["piece layers"] = self.piece_layers
    -        return info
    -
    -    def _traverse(self, path: str) -> dict:
    -        """
    -        Build meta dictionary while walking directory.
    -
    -        Parameters
    -        ----------
    -        path : str
    -            Path to target file.
    -        """
    -        if os.path.isfile(path):
    -            file_size = os.path.getsize(path)
    -            if self.hybrid:
    -                self.files.append(
    -                    {
    -                        "length": file_size,
    -                        "path": os.path.relpath(path, self.path).split(os.sep),
    -                    }
    -                )
    -
    -            if file_size == 0:
    -                return {"": {"length": file_size}}
    -
    -            logger.debug("Hashing %s", str(path))
    -            hasher = FileHasher(
    -                path, self.piece_length, progress=True, hybrid=self.hybrid
    -            )
    -            layers = bytearray()
    -            for result in hasher:
    -                if self.hybrid:
    -                    layer_hash, piece = result
    -                    self.pieces.extend(piece)
    -                else:
    -                    layer_hash = result
    -                layers.extend(layer_hash)
    -            if file_size > self.piece_length:
    -                self.piece_layers[hasher.root] = layers
    -            if self.hybrid and hasher.padding_file:
    -                self.files.append(hasher.padding_file)
    -
    -            return {"": {"length": file_size, "pieces root": hasher.root}}
    -
    -        tree = {}
    -        if os.path.isdir(path):
    -            for name in sorted(os.listdir(path)):
    -                tree[name] = self._traverse(os.path.join(path, name))
    -        return tree
    -
    -
    -
    @@ -32367,38 +13157,6 @@

    Create Bittorrent v1 v2 hybrid metafiles.

    -
    - Source code in torrentfile\torrent.py -
    658
    -659
    -660
    -661
    -662
    -663
    -664
    -665
    -666
    -667
    -668
    -669
    -670
    -671
    def __init__(self, **kwargs):
    -    """
    -    Create Bittorrent v1 v2 hybrid metafiles.
    -    """
    -    super().__init__(**kwargs)
    -    logger.debug("Assembling bittorrent Hybrid file")
    -    self.name = os.path.basename(self.path)
    -    self.hashes = []
    -    self.piece_layers = {}
    -    self.pieces = bytearray()
    -    self.files = []
    -    self.hybrid = self.meta_version == "3"
    -    self.total = len(utils.get_file_list(self.path))
    -    self.assemble()
    -
    -
    -
    @@ -32444,102 +13202,6 @@

    -
    - Source code in torrentfile\torrent.py -
    694
    -695
    -696
    -697
    -698
    -699
    -700
    -701
    -702
    -703
    -704
    -705
    -706
    -707
    -708
    -709
    -710
    -711
    -712
    -713
    -714
    -715
    -716
    -717
    -718
    -719
    -720
    -721
    -722
    -723
    -724
    -725
    -726
    -727
    -728
    -729
    -730
    -731
    -732
    -733
    -734
    -735
    -736
    -737
    -738
    -739
    def _traverse(self, path: str) -> dict:
    -    """
    -    Build meta dictionary while walking directory.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to target file.
    -    """
    -    if os.path.isfile(path):
    -        file_size = os.path.getsize(path)
    -        if self.hybrid:
    -            self.files.append(
    -                {
    -                    "length": file_size,
    -                    "path": os.path.relpath(path, self.path).split(os.sep),
    -                }
    -            )
    -
    -        if file_size == 0:
    -            return {"": {"length": file_size}}
    -
    -        logger.debug("Hashing %s", str(path))
    -        hasher = FileHasher(
    -            path, self.piece_length, progress=True, hybrid=self.hybrid
    -        )
    -        layers = bytearray()
    -        for result in hasher:
    -            if self.hybrid:
    -                layer_hash, piece = result
    -                self.pieces.extend(piece)
    -            else:
    -                layer_hash = result
    -            layers.extend(layer_hash)
    -        if file_size > self.piece_length:
    -            self.piece_layers[hasher.root] = layers
    -        if self.hybrid and hasher.padding_file:
    -            self.files.append(hasher.padding_file)
    -
    -        return {"": {"length": file_size, "pieces root": hasher.root}}
    -
    -    tree = {}
    -    if os.path.isdir(path):
    -        for name in sorted(os.listdir(path)):
    -            tree[name] = self._traverse(os.path.join(path, name))
    -    return tree
    -
    -
    -
    @@ -32561,50 +13223,6 @@

    Assemble the parts of the torrentfile into meta dictionary.

    -
    - Source code in torrentfile\torrent.py -
    673
    -674
    -675
    -676
    -677
    -678
    -679
    -680
    -681
    -682
    -683
    -684
    -685
    -686
    -687
    -688
    -689
    -690
    -691
    -692
    def assemble(self):
    -    """
    -    Assemble the parts of the torrentfile into meta dictionary.
    -    """
    -    info = self.meta["info"]
    -    info["meta version"] = 2
    -
    -    if os.path.isfile(self.path):
    -        info["file tree"] = {self.name: self._traverse(self.path)}
    -        info["length"] = os.path.getsize(self.path)
    -
    -    else:
    -        info["file tree"] = self._traverse(self.path)
    -        if self.hybrid:
    -            info["files"] = self.files
    -
    -    if self.hybrid:
    -        info["pieces"] = self.pieces
    -    self.meta["piece layers"] = self.piece_layers
    -    return info
    -
    -
    -
    @@ -32666,120 +13284,6 @@

    -
    - Source code in torrentfile\torrent.py -
    405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    class TorrentFile(MetaFile, ProgMixin):
    -    """
    -    Class for creating Bittorrent meta files.
    -
    -    Construct *Torrentfile* class instance object.
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        Dictionary containing torrent file options.
    -    """
    -
    -    hasher = Hasher
    -
    -    def __init__(self, **kwargs):
    -        """
    -        Construct TorrentFile instance with given keyword args.
    -
    -        Parameters
    -        ----------
    -        **kwargs : dict
    -            dictionary of keyword args passed to superclass.
    -        """
    -        super().__init__(**kwargs)
    -        logger.debug("Assembling bittorrent v1 torrent file")
    -        self.assemble()
    -
    -    def assemble(self):
    -        """
    -        Assemble components of torrent metafile.
    -
    -        Returns
    -        -------
    -        dict
    -            metadata dictionary for torrent file
    -        """
    -        info = self.meta["info"]
    -        size, filelist = utils.filelist_total(self.path)
    -        if os.path.isfile(self.path):
    -            info["length"] = size
    -        else:
    -            info["files"] = [
    -                {
    -                    "length": os.path.getsize(path),
    -                    "path": os.path.relpath(path, self.path).split(os.sep),
    -                }
    -                for path in filelist
    -            ]
    -        pieces = bytearray()
    -
    -        feeder = Hasher(filelist, self.piece_length, self.progress)
    -        for piece in feeder:
    -            pieces.extend(piece)
    -
    -        info["pieces"] = pieces
    -
    -
    -
    @@ -32832,34 +13336,6 @@

    -
    - Source code in torrentfile\torrent.py -
    419
    -420
    -421
    -422
    -423
    -424
    -425
    -426
    -427
    -428
    -429
    -430
    def __init__(self, **kwargs):
    -    """
    -    Construct TorrentFile instance with given keyword args.
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        dictionary of keyword args passed to superclass.
    -    """
    -    super().__init__(**kwargs)
    -    logger.debug("Assembling bittorrent v1 torrent file")
    -    self.assemble()
    -
    -
    -
    @@ -32899,66 +13375,6 @@

    -
    - Source code in torrentfile\torrent.py -
    432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    -449
    -450
    -451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    def assemble(self):
    -    """
    -    Assemble components of torrent metafile.
    -
    -    Returns
    -    -------
    -    dict
    -        metadata dictionary for torrent file
    -    """
    -    info = self.meta["info"]
    -    size, filelist = utils.filelist_total(self.path)
    -    if os.path.isfile(self.path):
    -        info["length"] = size
    -    else:
    -        info["files"] = [
    -            {
    -                "length": os.path.getsize(path),
    -                "path": os.path.relpath(path, self.path).split(os.sep),
    -            }
    -            for path in filelist
    -        ]
    -    pieces = bytearray()
    -
    -    feeder = Hasher(filelist, self.piece_length, self.progress)
    -    for piece in feeder:
    -        pieces.extend(piece)
    -
    -    info["pieces"] = pieces
    -
    -
    -
    @@ -33020,196 +13436,6 @@

    -
    - Source code in torrentfile\torrent.py -
    548
    -549
    -550
    -551
    -552
    -553
    -554
    -555
    -556
    -557
    -558
    -559
    -560
    -561
    -562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    -572
    -573
    -574
    -575
    -576
    -577
    -578
    -579
    -580
    -581
    -582
    -583
    -584
    -585
    -586
    -587
    -588
    -589
    -590
    -591
    -592
    -593
    -594
    -595
    -596
    -597
    -598
    -599
    -600
    -601
    -602
    -603
    -604
    -605
    -606
    -607
    -608
    -609
    -610
    -611
    -612
    -613
    -614
    -615
    -616
    -617
    -618
    -619
    -620
    -621
    -622
    -623
    -624
    -625
    -626
    -627
    -628
    -629
    -630
    -631
    -632
    -633
    -634
    -635
    -636
    -637
    -638
    -639
    -640
    class TorrentFileHybrid(MetaFile, ProgMixin):
    -    """
    -    Construct the Hybrid torrent meta file with provided parameters.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        Keyword arguments for torrent options.
    -    """
    -
    -    hasher = HasherHybrid
    -
    -    def __init__(self, **kwargs):
    -        """
    -        Create Bittorrent v1 v2 hybrid metafiles.
    -        """
    -        super().__init__(**kwargs)
    -        logger.debug("Assembling bittorrent Hybrid file")
    -        self.name = os.path.basename(self.path)
    -        self.hashes = []
    -        self.piece_layers = {}
    -        self.pieces = []
    -        self.files = []
    -        self.total = len(utils.get_file_list(self.path))
    -        self.assemble()
    -
    -    def assemble(self):
    -        """
    -        Assemble the parts of the torrentfile into meta dictionary.
    -
    -        **DEPRECATED**
    -        """
    -        info = self.meta["info"]
    -        info["meta version"] = 2
    -
    -        if os.path.isfile(self.path):
    -            info["file tree"] = {self.name: self._traverse(self.path)}
    -            info["length"] = os.path.getsize(self.path)
    -
    -        else:
    -            info["file tree"] = self._traverse(self.path)
    -            info["files"] = self.files
    -
    -        info["pieces"] = b"".join(self.pieces)
    -        self.meta["piece layers"] = self.piece_layers
    -        return info
    -
    -    def _traverse(self, path: str) -> dict:
    -        """
    -        Build meta dictionary while walking directory.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        path : str
    -            Path to target file.
    -        """
    -        if os.path.isfile(path):
    -            file_size = os.path.getsize(path)
    -
    -            self.files.append(
    -                {
    -                    "length": file_size,
    -                    "path": os.path.relpath(path, self.path).split(os.sep),
    -                }
    -            )
    -
    -            if file_size == 0:
    -                return {"": {"length": file_size}}
    -
    -            logger.debug("Hashing %s", str(path))
    -            file_hash = HasherHybrid(path, self.piece_length, self.progress)
    -            self.prog_update(file_size)
    -
    -            if file_size > self.piece_length:
    -                self.piece_layers[file_hash.root] = file_hash.piece_layer
    -
    -            self.hashes.append(file_hash)
    -            self.pieces.extend(file_hash.pieces)
    -
    -            if file_hash.padding_file:
    -                self.files.append(file_hash.padding_file)
    -
    -            return {"": {"length": file_size, "pieces root": file_hash.root}}
    -
    -        tree = {}
    -        if os.path.isdir(path):
    -            for name in sorted(os.listdir(path)):
    -                tree[name] = self._traverse(os.path.join(path, name))
    -        return tree
    -
    -
    -
    @@ -33244,36 +13470,6 @@

    Create Bittorrent v1 v2 hybrid metafiles.

    -
    - Source code in torrentfile\torrent.py -
    562
    -563
    -564
    -565
    -566
    -567
    -568
    -569
    -570
    -571
    -572
    -573
    -574
    def __init__(self, **kwargs):
    -    """
    -    Create Bittorrent v1 v2 hybrid metafiles.
    -    """
    -    super().__init__(**kwargs)
    -    logger.debug("Assembling bittorrent Hybrid file")
    -    self.name = os.path.basename(self.path)
    -    self.hashes = []
    -    self.piece_layers = {}
    -    self.pieces = []
    -    self.files = []
    -    self.total = len(utils.get_file_list(self.path))
    -    self.assemble()
    -
    -
    -
    @@ -33320,98 +13516,6 @@

    -
    - Source code in torrentfile\torrent.py -
    597
    -598
    -599
    -600
    -601
    -602
    -603
    -604
    -605
    -606
    -607
    -608
    -609
    -610
    -611
    -612
    -613
    -614
    -615
    -616
    -617
    -618
    -619
    -620
    -621
    -622
    -623
    -624
    -625
    -626
    -627
    -628
    -629
    -630
    -631
    -632
    -633
    -634
    -635
    -636
    -637
    -638
    -639
    -640
    def _traverse(self, path: str) -> dict:
    -    """
    -    Build meta dictionary while walking directory.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to target file.
    -    """
    -    if os.path.isfile(path):
    -        file_size = os.path.getsize(path)
    -
    -        self.files.append(
    -            {
    -                "length": file_size,
    -                "path": os.path.relpath(path, self.path).split(os.sep),
    -            }
    -        )
    -
    -        if file_size == 0:
    -            return {"": {"length": file_size}}
    -
    -        logger.debug("Hashing %s", str(path))
    -        file_hash = HasherHybrid(path, self.piece_length, self.progress)
    -        self.prog_update(file_size)
    -
    -        if file_size > self.piece_length:
    -            self.piece_layers[file_hash.root] = file_hash.piece_layer
    -
    -        self.hashes.append(file_hash)
    -        self.pieces.extend(file_hash.pieces)
    -
    -        if file_hash.padding_file:
    -            self.files.append(file_hash.padding_file)
    -
    -        return {"": {"length": file_size, "pieces root": file_hash.root}}
    -
    -    tree = {}
    -    if os.path.isdir(path):
    -        for name in sorted(os.listdir(path)):
    -            tree[name] = self._traverse(os.path.join(path, name))
    -    return tree
    -
    -
    -
    @@ -33434,50 +13538,6 @@

    Assemble the parts of the torrentfile into meta dictionary.

    DEPRECATED

    -
    - Source code in torrentfile\torrent.py -
    576
    -577
    -578
    -579
    -580
    -581
    -582
    -583
    -584
    -585
    -586
    -587
    -588
    -589
    -590
    -591
    -592
    -593
    -594
    -595
    def assemble(self):
    -    """
    -    Assemble the parts of the torrentfile into meta dictionary.
    -
    -    **DEPRECATED**
    -    """
    -    info = self.meta["info"]
    -    info["meta version"] = 2
    -
    -    if os.path.isfile(self.path):
    -        info["file tree"] = {self.name: self._traverse(self.path)}
    -        info["length"] = os.path.getsize(self.path)
    -
    -    else:
    -        info["file tree"] = self._traverse(self.path)
    -        info["files"] = self.files
    -
    -    info["pieces"] = b"".join(self.pieces)
    -    self.meta["piece layers"] = self.piece_layers
    -    return info
    -
    -
    -
    @@ -33539,178 +13599,6 @@

    -
    - Source code in torrentfile\torrent.py -
    462
    -463
    -464
    -465
    -466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    -493
    -494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    -515
    -516
    -517
    -518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    -544
    -545
    class TorrentFileV2(MetaFile, ProgMixin):
    -    """
    -    Class for creating Bittorrent meta v2 files.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        Keyword arguments for torrent file options.
    -    """
    -
    -    hasher = HasherV2
    -
    -    def __init__(self, **kwargs):
    -        """
    -        Construct `TorrentFileV2` Class instance from given parameters.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        **kwargs : dict
    -            keywword arguments to pass to superclass.
    -        """
    -        super().__init__(**kwargs)
    -        logger.debug("Assembling bittorrent v2 torrent file")
    -        self.piece_layers = {}
    -        self.hashes = []
    -        self.total = len(utils.get_file_list(self.path))
    -        self.assemble()
    -
    -    def assemble(self):
    -        """
    -        Assemble then return the meta dictionary for encoding.
    -
    -        **DEPRECATED**
    -
    -        Returns
    -        -------
    -        meta : dict
    -            Metainformation about the torrent.
    -        """
    -        info = self.meta["info"]
    -        if os.path.isfile(self.path):
    -            info["file tree"] = {info["name"]: self._traverse(self.path)}
    -            info["length"] = os.path.getsize(self.path)
    -            self.prog_update(info["length"])
    -        else:
    -            info["file tree"] = self._traverse(self.path)
    -
    -        info["meta version"] = 2
    -        self.meta["piece layers"] = self.piece_layers
    -
    -    def _traverse(self, path: str) -> dict:
    -        """
    -        Walk directory tree.
    -
    -        **DEPRECATED**
    -
    -        Parameters
    -        ----------
    -        path : str
    -            Path to file or directory.
    -        """
    -        if os.path.isfile(path):
    -            # Calculate Size and hashes for each file.
    -            size = os.path.getsize(path)
    -
    -            if size == 0:
    -                return {"": {"length": size}}
    -
    -            logger.debug("Hashing %s", str(path))
    -            fhash = HasherV2(path, self.piece_length, self.progress)
    -
    -            if size > self.piece_length:
    -                self.piece_layers[fhash.root] = fhash.piece_layer
    -            return {"": {"length": size, "pieces root": fhash.root}}
    -
    -        file_tree = {}
    -        if os.path.isdir(path):
    -            for name in sorted(os.listdir(path)):
    -                file_tree[name] = self._traverse(os.path.join(path, name))
    -        return file_tree
    -
    -
    -
    @@ -33767,44 +13655,6 @@

    -
    - Source code in torrentfile\torrent.py -
    476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    -484
    -485
    -486
    -487
    -488
    -489
    -490
    -491
    -492
    def __init__(self, **kwargs):
    -    """
    -    Construct `TorrentFileV2` Class instance from given parameters.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    **kwargs : dict
    -        keywword arguments to pass to superclass.
    -    """
    -    super().__init__(**kwargs)
    -    logger.debug("Assembling bittorrent v2 torrent file")
    -    self.piece_layers = {}
    -    self.hashes = []
    -    self.total = len(utils.get_file_list(self.path))
    -    self.assemble()
    -
    -
    -
    @@ -33851,70 +13701,6 @@

    -
    - Source code in torrentfile\torrent.py -
    516
    -517
    -518
    -519
    -520
    -521
    -522
    -523
    -524
    -525
    -526
    -527
    -528
    -529
    -530
    -531
    -532
    -533
    -534
    -535
    -536
    -537
    -538
    -539
    -540
    -541
    -542
    -543
    -544
    -545
    def _traverse(self, path: str) -> dict:
    -    """
    -    Walk directory tree.
    -
    -    **DEPRECATED**
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to file or directory.
    -    """
    -    if os.path.isfile(path):
    -        # Calculate Size and hashes for each file.
    -        size = os.path.getsize(path)
    -
    -        if size == 0:
    -            return {"": {"length": size}}
    -
    -        logger.debug("Hashing %s", str(path))
    -        fhash = HasherV2(path, self.piece_length, self.progress)
    -
    -        if size > self.piece_length:
    -            self.piece_layers[fhash.root] = fhash.piece_layer
    -        return {"": {"length": size, "pieces root": fhash.root}}
    -
    -    file_tree = {}
    -    if os.path.isdir(path):
    -        for name in sorted(os.listdir(path)):
    -            file_tree[name] = self._traverse(os.path.join(path, name))
    -    return file_tree
    -
    -
    -
    @@ -33955,52 +13741,6 @@

    -
    - Source code in torrentfile\torrent.py -
    494
    -495
    -496
    -497
    -498
    -499
    -500
    -501
    -502
    -503
    -504
    -505
    -506
    -507
    -508
    -509
    -510
    -511
    -512
    -513
    -514
    def assemble(self):
    -    """
    -    Assemble then return the meta dictionary for encoding.
    -
    -    **DEPRECATED**
    -
    -    Returns
    -    -------
    -    meta : dict
    -        Metainformation about the torrent.
    -    """
    -    info = self.meta["info"]
    -    if os.path.isfile(self.path):
    -        info["file tree"] = {info["name"]: self._traverse(self.path)}
    -        info["length"] = os.path.getsize(self.path)
    -        self.prog_update(info["length"])
    -    else:
    -        info["file tree"] = self._traverse(self.path)
    -
    -    info["meta version"] = 2
    -    self.meta["piece layers"] = self.piece_layers
    -
    -
    -
    @@ -34030,15 +13770,7 @@

    - - -

    - torrentfile.utils - - - -

    - +

    Utility functions and classes used throughout package.

    @@ -34106,86 +13838,6 @@

    -
    - Source code in torrentfile\utils.py -
    41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    -77
    -78
    class Memo:
    -    """
    -    Memoice chache object.
    -
    -    Parameters
    -    ----------
    -    func : function
    -        The function that is being memoized.
    -    """
    -
    -    def __init__(self, func):
    -        """
    -        Construct for memoization.
    -        """
    -        self.func = func
    -        self.counter = 0
    -        self.cache = {}
    -
    -    def __call__(self, path):
    -        """
    -        Invoke each time memo function is called.
    -
    -        Parameters
    -        ----------
    -        path : str
    -            The relative or absolute path being used as key in cache dict.
    -
    -        Returns
    -        -------
    -        Any :
    -            The results of calling the function with path.
    -        """
    -        if path in self.cache and os.path.exists(path):
    -            self.counter += 1
    -            return self.cache[path]
    -        result = self.func(path)
    -        self.cache[path] = result
    -        return result
    -
    -
    -
    @@ -34257,50 +13909,6 @@

    -
    - Source code in torrentfile\utils.py -
    59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    -77
    -78
    def __call__(self, path):
    -    """
    -    Invoke each time memo function is called.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        The relative or absolute path being used as key in cache dict.
    -
    -    Returns
    -    -------
    -    Any :
    -        The results of calling the function with path.
    -    """
    -    if path in self.cache and os.path.exists(path):
    -        self.counter += 1
    -        return self.cache[path]
    -    result = self.func(path)
    -    self.cache[path] = result
    -    return result
    -
    -
    -

    @@ -34322,24 +13930,6 @@

    Construct for memoization.

    -
    - Source code in torrentfile\utils.py -
    51
    -52
    -53
    -54
    -55
    -56
    -57
    def __init__(self, func):
    -    """
    -    Construct for memoization.
    -    """
    -    self.func = func
    -    self.counter = 0
    -    self.cache = {}
    -
    -
    -
    @@ -34401,50 +13991,6 @@

    -
    - Source code in torrentfile\utils.py -
     81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    class MissingPathError(Exception):
    -    """
    -    Path parameter is required to specify target content.
    -
    -    Creating a .torrent file with no contents seems rather silly.
    -
    -    Parameters
    -    ----------
    -    message : str
    -        Message for user (optional).
    -    """
    -
    -    def __init__(self, message: str = None):
    -        """
    -        Raise when creating a meta file without specifying target content.
    -
    -        The `message` argument is a message to pass to Exception base class.
    -        """
    -        self.message = f"Path arguement is missing and required {str(message)}"
    -        super().__init__(message)
    -
    -
    -
    @@ -34474,26 +14020,6 @@

    Raise when creating a meta file without specifying target content.

    The message argument is a message to pass to Exception base class.

    -
    - Source code in torrentfile\utils.py -
     93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    def __init__(self, message: str = None):
    -    """
    -    Raise when creating a meta file without specifying target content.
    -
    -    The `message` argument is a message to pass to Exception base class.
    -    """
    -    self.message = f"Path arguement is missing and required {str(message)}"
    -    super().__init__(message)
    -
    -
    -
    @@ -34554,46 +14080,6 @@

    -
    - Source code in torrentfile\utils.py -
    103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    class PieceLengthValueError(Exception):
    -    """
    -    Piece Length parameter must equal a perfect power of 2.
    -
    -    Parameters
    -    ----------
    -    message : str
    -        Message for user (optional).
    -    """
    -
    -    def __init__(self, message: str = None):
    -        """
    -        Raise when creating a meta file with incorrect piece length value.
    -
    -        The `message` argument is a message to pass to Exception base class.
    -        """
    -        self.message = f"Incorrect value for piece length: {str(message)}"
    -        super().__init__(message)
    -
    -
    -
    @@ -34623,26 +14109,6 @@

    - Source code in torrentfile\utils.py -
    113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    def __init__(self, message: str = None):
    -    """
    -    Raise when creating a meta file with incorrect piece length value.
    -
    -    The `message` argument is a message to pass to Exception base class.
    -    """
    -    self.message = f"Incorrect value for piece length: {str(message)}"
    -    super().__init__(message)
    -
    -
    - @@ -34723,64 +14189,6 @@

    -
    - Source code in torrentfile\utils.py -
    228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    def _filelist_total(path: str) -> tuple:
    -    """
    -    Search directory tree for files.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        Path to file or directory base
    -
    -    Returns
    -    -------
    -    int
    -        Sum of all filesizes in filelist.
    -    list
    -        All file paths within directory tree.
    -    """
    -    if path.is_file():
    -        file_size = os.path.getsize(path)
    -        return file_size, [str(path)]
    -    total = 0
    -    filelist = []
    -    if path.is_dir():
    -        for item in path.iterdir():
    -            size, paths = filelist_total(item)
    -            total += size
    -            filelist.extend(paths)
    -    return total, sorted(filelist)
    -
    -
    -
    @@ -34862,58 +14270,6 @@

    -
    - Source code in torrentfile\utils.py -
    202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    @Memo
    -def filelist_total(pathstring: str) -> os.PathLike:
    -    """
    -    Perform error checking and format conversion to os.PathLike.
    -
    -    Parameters
    -    ----------
    -    pathstring : str
    -        An existing filesystem path.
    -
    -    Returns
    -    -------
    -    os.PathLike
    -        Input path converted to bytes format.
    -
    -    Raises
    -    ------
    -    MissingPathError
    -        File could not be found.
    -    """
    -    if os.path.exists(pathstring):
    -        path = Path(pathstring)
    -        return _filelist_total(path)
    -    raise MissingPathError
    -
    -
    -
    @@ -34977,42 +14333,6 @@

    -
    - Source code in torrentfile\utils.py -
    275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    def get_file_list(path: str) -> list:
    -    """
    -    Return a sorted list of file paths contained in directory.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        target file or directory.
    -
    -    Returns
    -    -------
    -    list
    -        sorted list of file paths.
    -    """
    -    _, filelist = filelist_total(path)
    -    return filelist
    -
    -
    -
    @@ -35076,46 +14396,6 @@

    -
    - Source code in torrentfile\utils.py -
    182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    def get_piece_length(size: int) -> int:
    -    """
    -    Calculate the ideal piece length for bittorrent data.
    -
    -    Parameters
    -    ----------
    -    size : int
    -        Total bits of all files incluided in .torrent file.
    -
    -    Returns
    -    -------
    -    int
    -        Ideal piece length.
    -    """
    -    exp = 14
    -    while size / (2**exp) > 200 and exp < 25:
    -        exp += 1
    -    return 2**exp
    -
    -
    -
    @@ -35179,52 +14459,6 @@

    -
    - Source code in torrentfile\utils.py -
    123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    def humanize_bytes(amount: int) -> str:
    -    """
    -    Convert integer into human readable memory sized denomination.
    -
    -    Parameters
    -    ----------
    -    amount : int
    -        total number of bytes.
    -
    -    Returns
    -    -------
    -    str
    -        human readable representation of the given amount of bytes.
    -    """
    -    if amount < 1024:
    -        return str(amount)
    -    if 1024 <= amount < 1_048_576:
    -        return f"{amount // 1024} KiB"
    -    if 1_048_576 <= amount < 1_073_741_824:
    -        return f"{amount // 1_048_576} MiB"
    -    return f"{amount // 1073741824} GiB"
    -
    -
    -
    @@ -35288,50 +14522,6 @@

    -
    - Source code in torrentfile\utils.py -
    334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    -350
    -351
    -352
    -353
    def next_power_2(value: int) -> int:
    -    """
    -    Calculate the next perfect power of 2 equal to or greater than value.
    -
    -    Parameters
    -    ----------
    -    value : int
    -        integer value that is less than some perfect power of 2.
    -
    -    Returns
    -    -------
    -    int
    -        The next power of 2 greater than value, or value if already power of 2.
    -    """
    -    if not value & (value - 1) and value:
    -        return value
    -    start = 1
    -    while start < value:
    -        start <<= 1
    -    return start
    -
    -
    -
    @@ -35413,78 +14603,6 @@

    -
    - Source code in torrentfile\utils.py -
    146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    def normalize_piece_length(piece_length: int) -> int:
    -    """
    -    Verify input piece_length is valid and convert accordingly.
    -
    -    Parameters
    -    ----------
    -    piece_length : int | str
    -        The piece length provided by user.
    -
    -    Returns
    -    -------
    -    int
    -        normalized piece length.
    -
    -    Raises
    -    ------
    -    PieceLengthValueError :
    -        If piece length is improper value.
    -    """
    -    if isinstance(piece_length, str):
    -        if piece_length.isnumeric():
    -            piece_length = int(piece_length)
    -        else:
    -            raise PieceLengthValueError(piece_length)
    -
    -    if 13 < piece_length < 26:
    -        return 2**piece_length
    -    if piece_length <= 13:
    -        raise PieceLengthValueError(piece_length)
    -
    -    log = int(math.log2(piece_length))
    -    if 2**log == piece_length:
    -        return piece_length
    -    raise PieceLengthValueError
    -
    -
    -
    @@ -35548,42 +14666,6 @@

    -
    - Source code in torrentfile\utils.py -
    316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    -329
    -330
    -331
    def path_piece_length(path: str) -> int:
    -    """
    -    Calculate piece length for input path and contents.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        The absolute path to directory and contents.
    -
    -    Returns
    -    -------
    -    int
    -        The size of pieces of torrent content.
    -    """
    -    psize = path_size(path)
    -    return get_piece_length(psize)
    -
    -
    -
    @@ -35647,42 +14729,6 @@

    -
    - Source code in torrentfile\utils.py -
    257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    def path_size(path: str) -> int:
    -    """
    -    Return the total size of all files in path recursively.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        path to target file or directory.
    -
    -    Returns
    -    -------
    -    int
    -        total size of files.
    -    """
    -    total_size, _ = filelist_total(path)
    -    return total_size
    -
    -
    -
    @@ -35758,52 +14804,6 @@

    -
    - Source code in torrentfile\utils.py -
    293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    def path_stat(path: str) -> tuple:
    -    """
    -    Calculate directory statistics.
    -
    -    Parameters
    -    ----------
    -    path : str
    -        The path to start calculating from.
    -
    -    Returns
    -    -------
    -    list
    -        List of all files contained in Directory
    -    int
    -        Total sum of bytes from all contents of dir
    -    int
    -        The size of pieces of the torrent contents.
    -    """
    -    total_size, filelist = filelist_total(path)
    -    piece_length = get_piece_length(total_size)
    -    return (filelist, total_size, piece_length)
    -
    -
    -
    @@ -35822,15 +14822,7 @@

    - - -

    - torrentfile.version - - - -

    - +

    Holds the release version number.

    @@ -35849,7012 +14841,6 @@

    -

    - -
    - - - - - -
    - - - -

    - tests - - - -

    - -
    - -

    Unittest package init module.

    - - - -
    - - - - - - - - -
    - - - -

    -dir1() - - -

    - - -
    - -

    Create a specific temporary structured directory.

    - -

    Yields:

    - - - - - - - - - - - - - -
    TypeDescription
    - str -

    path to root of temporary directory

    - -
    - Source code in tests\__init__.py -
    168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    @pytest.fixture(scope="package")
    -def dir1():
    -    """Create a specific temporary structured directory.
    -
    -    Yields
    -    ------
    -    str
    -        path to root of temporary directory
    -    """
    -    root = tempdir()
    -    yield root
    -    rmpath(root)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -dir2() - - -

    - - -
    - -

    Create a specific temporary structured directory.

    - -

    Yields:

    - - - - - - - - - - - - - -
    TypeDescription
    - str -

    path to root of temporary directory

    - -
    - Source code in tests\__init__.py -
    182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    @pytest.fixture()
    -def dir2():
    -    """Create a specific temporary structured directory.
    -
    -    Yields
    -    ------
    -    str
    -        path to root of temporary directory
    -    """
    -    root = tempdir(ext="2")
    -    yield Path(root)
    -    rmpath(root)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -file1() - - -

    - - -
    - -

    Return the path to a temporary file package scope.

    - -
    - Source code in tests\__init__.py -
    241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    @pytest.fixture(scope="package")
    -def file1():
    -    """
    -    Return the path to a temporary file package scope.
    -    """
    -    path = tempfile()
    -    yield path
    -    rmpath(path)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -file2() - - -

    - - -
    - -

    Return the path to a temporary file no scope.

    - -
    - Source code in tests\__init__.py -
    297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    @pytest.fixture()
    -def file2():
    -    """
    -    Return the path to a temporary file no scope.
    -    """
    -    path = tempfile()
    -    yield path
    -    rmpath(path)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -filemeta1(file1, request) - - -

    - - -
    - -

    Test fixture for generating metafile for all versions of torrents.

    - -
    - Source code in tests\__init__.py -
    251
    -252
    -253
    -254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    @pytest.fixture(scope="package", params=torrents())
    -def filemeta1(file1, request):
    -    """
    -    Test fixture for generating metafile for all versions of torrents.
    -    """
    -    args = {
    -        "path": file1,
    -        "announce": ["url1", "url4"],
    -        "url_list": ["url6", "url7"],
    -        "httpseeds": ["url6", "url7"],
    -        "comment": "this is a comment",
    -        "source": "SomeSource",
    -        "private": 1,
    -    }
    -    versions = torrents()
    -    version = versions.index(request.param)
    -    name = str(file1) + "file" + str(version) + ".torrent"
    -    torrent = request.param(**args)
    -    outfile, _ = torrent.write(outfile=name)
    -    yield outfile
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -filemeta2(file2, request) - - -

    - - -
    - -

    Test fixture for generating a meta file no scope.

    - -
    - Source code in tests\__init__.py -
    274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    @pytest.fixture(params=torrents())
    -def filemeta2(file2, request):
    -    """
    -    Test fixture for generating a meta file no scope.
    -    """
    -    args = {
    -        "path": file2,
    -        "announce": ["url1", "url4"],
    -        "url_list": ["url6", "url7"],
    -        "httpseeds": ["url7", "url8"],
    -        "comment": "this is a comment",
    -        "source": "SomeSource",
    -        "private": 1,
    -    }
    -    versions = torrents()
    -    version = versions.index(request.param)
    -    name = str(file2) + "file" + str(version) + ".torrent"
    -    torrent = request.param(**args)
    -    outfile, _ = torrent.write(outfile=name)
    -    yield outfile
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -metafile1(dir1, request) - - -

    - - -
    - -

    Create a standard metafile for testing.

    - -
    - Source code in tests\__init__.py -
    196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    @pytest.fixture(scope="package", params=torrents())
    -def metafile1(dir1, request):
    -    """
    -    Create a standard metafile for testing.
    -    """
    -    versions = torrents()
    -    args = {
    -        "path": dir1,
    -        "announce": ["url1", "url2", "url4"],
    -        "url_list": ["url5", "url6", "url7"],
    -        "httpseeds": ["url5", "url6", "url7"],
    -        "comment": "this is a comment",
    -        "source": "SomeSource",
    -        "private": 1,
    -    }
    -    torrent_class = request.param
    -    outfile = str(dir1) + str(versions.index(torrent_class)) + ".torrent"
    -    torrent = torrent_class(**args)
    -    outfile, _ = torrent.write(outfile=outfile)
    -    yield outfile
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -metafile2(dir2, request) - - -

    - - -
    - -

    Create a standard metafile for testing.

    - -
    - Source code in tests\__init__.py -
    219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    @pytest.fixture(params=torrents())
    -def metafile2(dir2, request):
    -    """
    -    Create a standard metafile for testing.
    -    """
    -    args = {
    -        "path": dir2,
    -        "announce": ["url1", "url4"],
    -        "url_list": ["url6", "url7"],
    -        "comment": "this is a comment",
    -        "httpseeds": ["url6", "url7"],
    -        "source": "SomeSource",
    -        "private": 1,
    -    }
    -    torrent_class = request.param
    -    outfile = str(dir2) + ".torrent"
    -    torrent = torrent_class(**args)
    -    outfile, _ = torrent.write(outfile=outfile)
    -    yield outfile
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -rmpath(*args) - - -

    - - -
    - -

    Remove file or directory path.

    - -

    Parameters:

    - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionDefault
    *args - list -

    Filesystem locations for removing.

    - () -
    - -
    - Source code in tests\__init__.py -
     80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    def rmpath(*args):
    -    """Remove file or directory path.
    -
    -    Parameters
    -    ----------
    -    *args : list
    -        Filesystem locations for removing.
    -    """
    -    for arg in args:
    -        if not os.path.exists(arg):
    -            continue
    -        if os.path.isdir(arg):
    -            try:
    -                shutil.rmtree(arg)
    -            except PermissionError:  # pragma: nocover
    -                pass
    -        elif os.path.isfile(arg):
    -            try:
    -                os.remove(arg)
    -            except PermissionError:  # pragma: nocover
    -                pass
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -sizedfiles(dir2, sizes, request) - - -

    - - -
    - -

    Generate variable sized meta files for testing, no scope.

    - -
    - Source code in tests\__init__.py -
    307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    -326
    -327
    -328
    @pytest.fixture(params=torrents())
    -def sizedfiles(dir2, sizes, request):
    -    """
    -    Generate variable sized meta files for testing, no scope.
    -    """
    -    versions = torrents()
    -    args = {
    -        "content": dir2,
    -        "announce": ["url1", "url2", "url4"],
    -        "url_list": ["url5", "url6", "url7"],
    -        "comment": "this is a comment",
    -        "source": "SomeSource",
    -        "private": 1,
    -        "piece_length": sizes,
    -    }
    -    torrent_class = request.param
    -    version = str(versions.index(torrent_class))
    -    outfile = str(dir2) + version + str(sizes) + ".torrent"
    -    torrent = torrent_class(**args)
    -    outfile, _ = torrent.write(outfile=outfile)
    -    yield outfile
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -sizes(request) - - -

    - - -
    - -

    Generate powers of 2 for file creation package scope.

    - -
    - Source code in tests\__init__.py -
    159
    -160
    -161
    -162
    -163
    -164
    -165
    @pytest.fixture(scope="package", params=[2**i for i in range(15, 20)])
    -def sizes(request):
    -    """
    -    Generate powers of 2 for file creation package scope.
    -    """
    -    size = request.param
    -    yield size
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -teardown() - - -

    - - -
    - -

    Remove all temporary directories and files.

    - -
    - Source code in tests\__init__.py -
    141
    -142
    -143
    -144
    -145
    -146
    -147
    -148
    -149
    @atexit.register
    -def teardown():  # pragma: nocover
    -    """
    -    Remove all temporary directories and files.
    -    """
    -    root = Path(__file__).parent / "TESTDIR"
    -    for path in [root, "./torrentfile.log"]:
    -        if os.path.exists(path):
    -            rmpath(path)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -tempdir(ext='1') - - -

    - - -
    - -

    Create temporary directory.

    - -

    Parameters:

    - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionDefault
    ext - str, optional -

    extension to file names, by default "1"

    - '1' -
    - -

    Returns:

    - - - - - - - - - - - - - -
    TypeDescription
    - str -

    path to common root for directory.

    - -
    - Source code in tests\__init__.py -
    103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    def tempdir(ext="1"):
    -    """Create temporary directory.
    -
    -    Parameters
    -    ----------
    -    ext : str, optional
    -        extension to file names, by default "1"
    -
    -    Returns
    -    -------
    -    str
    -        path to common root for directory.
    -    """
    -    layouts = {
    -        "1": [
    -            f"dir{ext}/file1.png",
    -            f"dir{ext}/file2.mp4",
    -            f"dir{ext}/file3.mp3",
    -            f"dir{ext}/file4.zip",
    -            f"dir{ext}/file5.txt",
    -            f"dir{ext}/subdir1/subdir2/file.7z",
    -            f"dir{ext}/subdir/subdir/file4.rar",
    -            f"dir{ext}/subdir/subdir/file4.r01",
    -        ],
    -        "2": [
    -            f"dir{ext}/file1.png",
    -            f"dir{ext}/file2.jpg",
    -            f"dir{ext}/subdir/file2.mp4",
    -            f"dir{ext}/subdir/file3.mp3",
    -        ],
    -    }
    -    paths = []
    -    for path in layouts[ext]:
    -        temps = tempfile(path=path, exp=18)
    -        paths.append(temps)
    -    return os.path.commonpath(paths)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -tempfile(path=None, exp=18) - - -

    - - -
    - -

    Create temporary file.

    -

    Creates a temporary file for unittesting purposes.py

    - -

    Parameters:

    - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionDefault
    path - str, optional -

    relative path to temporary files, by default None

    - None -
    exp - int, optional -

    Exponent used to determine size of file., by default 18

    - 18 -
    - -

    Returns:

    - - - - - - - - - - - - - -
    TypeDescription
    - str -

    absolute path to file.

    - -
    - Source code in tests\__init__.py -
    36
    -37
    -38
    -39
    -40
    -41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    -77
    def tempfile(path=None, exp=18):
    -    """Create temporary file.
    -
    -    Creates a temporary file for unittesting purposes.py
    -
    -    Parameters
    -    ----------
    -    path : str, optional
    -        relative path to temporary files, by default None
    -    exp : int, optional
    -        Exponent used to determine size of file., by default 18
    -
    -    Returns
    -    -------
    -    str
    -        absolute path to file.
    -    """
    -    seq = (string.printable + string.whitespace).encode("utf-8")
    -    root = Path(__file__).parent / "TESTDIR"
    -    if not os.path.exists(root):
    -        os.mkdir(root)
    -    if not path:
    -        path = root / (str(datetime.timestamp(datetime.now())) + ".file")
    -    parts = Path(path).parts
    -    partial = root
    -    for i, part in enumerate(parts):
    -        partial = partial / part
    -        if i == len(parts) - 1:
    -            with open(partial, "wb") as binfile:
    -                size = 2**exp
    -                while size > 0:
    -                    if len(seq) < size:
    -                        binfile.write(seq)
    -                        size -= len(seq)
    -                        seq += seq
    -                    else:
    -                        binfile.write(seq[:size])
    -                        size -= size
    -        else:
    -            if not os.path.exists(partial):
    -                os.mkdir(partial)
    -    return partial
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -torrents() - - -

    - - -
    - -

    Return seq of torrentfile objects.

    - -
    - Source code in tests\__init__.py -
    152
    -153
    -154
    -155
    -156
    def torrents():
    -    """
    -    Return seq of torrentfile objects.
    -    """
    -    return [TorrentFile, TorrentFileV2, TorrentFileHybrid, TorrentAssembler]
    -
    -
    -
    -
    - -
    - - - - -
    - - - -

    - test_cli - - - -

    - -
    - -

    Testing functions for the command line interface.

    - - - -
    - - - - - - - - -
    - - - -

    -folder(dir1) - - -

    - - -
    - -

    Yield a folder object as fixture.

    - -
    - Source code in tests\test_cli.py -
    41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    @pytest.fixture(scope="module")
    -def folder(dir1):
    -    """
    -    Yield a folder object as fixture.
    -    """
    -    sfolder = str(dir1)
    -    torrent = sfolder + ".torrent"
    -    yield (sfolder, torrent)
    -    rmpath(torrent)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_announce(folder, piece_length, version) - - -

    - - -
    - -

    Test announce cli flag.

    - -
    - Source code in tests\test_cli.py -
    122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    @pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
    -@pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_announce(folder, piece_length, version):
    -    """
    -    Test announce cli flag.
    -    """
    -    folder, torrent = folder
    -    args = [
    -        "torrentfile",
    -        "create",
    -        folder,
    -        "--piece-length",
    -        str(piece_length),
    -        "--meta-version",
    -        version,
    -        "--tracker",
    -        "https://announce.org/tracker",
    -    ]
    -    sys.argv = args
    -    execute()
    -    meta = pyben.load(torrent)
    -    assert meta["announce"] == "https://announce.org/tracker"
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_announce_list(folder, version) - - -

    - - -
    - -

    Test announce-list cli flag.

    - -
    - Source code in tests\test_cli.py -
    146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    @pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_announce_list(folder, version):
    -    """
    -    Test announce-list cli flag.
    -    """
    -    folder, torrent = folder
    -    trackers = [
    -        "https://announce.org/tracker",
    -        "https://announce.net/tracker",
    -        "https://tracker.net/announce",
    -    ]
    -    args = [
    -        "torrentfile",
    -        "create",
    -        folder,
    -        "--meta-version",
    -        version,
    -        "--tracker",
    -    ] + trackers
    -    sys.argv = args
    -    execute()
    -    meta = pyben.load(torrent)
    -    for url in trackers:
    -        assert url in [j for i in meta["announce-list"] for j in i]
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_announce_path(dir1, flag) - - -

    - - -
    - -

    Test CLI when path is placed after the trackers flag.

    - -
    - Source code in tests\test_cli.py -
    451
    -452
    -453
    -454
    -455
    -456
    -457
    -458
    -459
    -460
    -461
    -462
    -463
    @pytest.mark.parametrize(
    -    "flag", ["-t", "-w", "--announce", "--web-seed", "--http-seed"]
    -)
    -def test_cli_announce_path(dir1, flag):
    -    """
    -    Test CLI when path is placed after the trackers flag.
    -    """
    -    args = ["torrentfile", "create", flag, "https://announce1.org", str(dir1)]
    -    sys.argv = args
    -    execute()
    -    outfile = str(dir1) + ".torrent"
    -    assert os.path.exists(outfile)
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_comment(folder, piece_length, version) - - -

    - - -
    - -

    Test comment cli flag.

    - -
    - Source code in tests\test_cli.py -
    172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    @pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
    -@pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_comment(folder, piece_length, version):
    -    """
    -    Test comment cli flag.
    -    """
    -    folder, torrent = folder
    -    args = [
    -        "torrentfile",
    -        "create",
    -        folder,
    -        "--piece-length",
    -        str(piece_length),
    -        "--meta-version",
    -        version,
    -        "--magnet",
    -        "--comment",
    -        "this is a comment",
    -        "--progress",
    -        "1",
    -    ]
    -    sys.argv = args
    -    execute()
    -    meta = pyben.load(torrent)
    -    assert meta["info"]["comment"] == "this is a comment"
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_created_by(folder, piece_length, version) - - -

    - - -
    - -

    Test if created torrents recieve a created by field in meta info.

    - -
    - Source code in tests\test_cli.py -
    254
    -255
    -256
    -257
    -258
    -259
    -260
    -261
    -262
    -263
    -264
    -265
    -266
    -267
    -268
    -269
    -270
    -271
    -272
    -273
    -274
    -275
    @pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
    -@pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_created_by(folder, piece_length, version):
    -    """
    -    Test if created torrents recieve a created by field in meta info.
    -    """
    -    folder, torrent = folder
    -    args = [
    -        "torrentfile",
    -        "create",
    -        folder,
    -        "--piece-length",
    -        str(piece_length),
    -        "--meta-version",
    -        version,
    -        "--comment",
    -        "this is a comment",
    -    ]
    -    sys.argv = args
    -    execute()
    -    meta = pyben.load(torrent)
    -    assert "TorrentFile" in meta["created by"]
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_creation_date(folder, piece_length, version) - - -

    - - -
    - -

    Test if torrents created get an accurate timestamp.

    - -
    - Source code in tests\test_cli.py -
    225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    @pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
    -@pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_creation_date(folder, piece_length, version):
    -    """
    -    Test if torrents created get an accurate timestamp.
    -    """
    -    folder, torrent = folder
    -    args = [
    -        "torrentfile",
    -        "create",
    -        folder,
    -        "--piece-length",
    -        str(piece_length),
    -        "--meta-version",
    -        version,
    -        "--comment",
    -        "this is a comment",
    -    ]
    -    sys.argv = args
    -    execute()
    -    meta = pyben.load(torrent)
    -    num = float(meta["creation date"])
    -    date = datetime.datetime.fromtimestamp(num)
    -    now = datetime.datetime.now()
    -    assert date.day == now.day
    -    assert date.year == now.year
    -    assert date.month == now.month
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_cwd(folder) - - -

    - - -
    - -

    Test outfile cli flag.

    - -
    - Source code in tests\test_cli.py -
    466
    -467
    -468
    -469
    -470
    -471
    -472
    -473
    -474
    -475
    -476
    -477
    -478
    -479
    -480
    -481
    -482
    -483
    def test_cli_cwd(folder):
    -    """
    -    Test outfile cli flag.
    -    """
    -    folder, _ = folder
    -    args = [
    -        "torrentfile",
    -        "create",
    -        "--cwd",
    -        folder,
    -    ]
    -    sys.argv = args
    -    current = os.getcwd()
    -    name = os.path.basename(folder)
    -    outfile = os.path.join(current, name) + ".torrent"
    -    execute()
    -    assert os.path.exists(outfile)
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_empty_files(dir2, version, progress) - - -

    - - -
    - -

    Test creating torrent with empty files.

    - -
    - Source code in tests\test_cli.py -
    364
    -365
    -366
    -367
    -368
    -369
    -370
    -371
    -372
    -373
    -374
    -375
    -376
    -377
    -378
    -379
    -380
    -381
    -382
    -383
    -384
    -385
    -386
    -387
    -388
    -389
    -390
    -391
    -392
    -393
    -394
    -395
    -396
    -397
    -398
    -399
    -400
    -401
    @pytest.mark.parametrize("version", ["1", "2", "3"])
    -@pytest.mark.parametrize("progress", ["0", "1"])
    -def test_cli_empty_files(dir2, version, progress):
    -    """
    -    Test creating torrent with empty files.
    -    """
    -    args = [
    -        "torrentfile",
    -        "create",
    -        str(dir2),
    -        "--meta-version",
    -        version,
    -        "--source",
    -        "somesource",
    -        "--prog",
    -        progress,
    -    ]
    -    sys.argv = args
    -
    -    def walk(root, count):
    -        """
    -        Traverse directory to edit files.
    -        """
    -        if root.is_file():
    -            with open(root, "wb") as _:
    -                return 1
    -        elif root.is_dir():
    -            for item in root.iterdir():
    -                if count >= 2:
    -                    break
    -                count += walk(item, count)
    -        return count
    -
    -    walk(dir2, 0)
    -    execute()
    -    outfile = str(dir2) + ".torrent"
    -    assert os.path.exists(outfile)
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_help() - - -

    - - -
    - -

    Test showing help notice cli flag.

    - -
    - Source code in tests\test_cli.py -
    352
    -353
    -354
    -355
    -356
    -357
    -358
    -359
    -360
    -361
    def test_cli_help():
    -    """
    -    Test showing help notice cli flag.
    -    """
    -    args = ["-h"]
    -    sys.argv = args
    -    try:
    -        assert execute()
    -    except SystemExit:
    -        assert True
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_outfile(dir1, piece_length, version) - - -

    - - -
    - -

    Test outfile cli flag.

    - -
    - Source code in tests\test_cli.py -
    199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    @pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
    -@pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_outfile(dir1, piece_length, version):
    -    """
    -    Test outfile cli flag.
    -    """
    -    outfile = dir1 + "test.torrent"
    -    args = [
    -        "torrentfile",
    -        "create",
    -        dir1,
    -        "--piece-length",
    -        str(piece_length),
    -        "--meta-version",
    -        version,
    -        "-o",
    -        outfile,
    -        "--prog",
    -        "1",
    -    ]
    -    sys.argv = args
    -    execute()
    -    assert os.path.exists(outfile)
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_piece_length(folder, piece_length, version) - - -

    - - -
    - -

    Test piece length cli flag.

    - -
    - Source code in tests\test_cli.py -
     97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    @pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
    -@pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_piece_length(folder, piece_length, version):
    -    """
    -    Test piece length cli flag.
    -    """
    -    folder, torrent = folder
    -    args = [
    -        "torrentfile",
    -        "-v",
    -        "create",
    -        folder,
    -        "--piece-length",
    -        str(piece_length),
    -        "--meta-version",
    -        version,
    -        "--progress",
    -        "0",
    -    ]
    -    sys.argv = args
    -    execute()
    -    meta = pyben.load(torrent)
    -    assert meta["info"]["piece length"] == piece_length
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_private(folder) - - -

    - - -
    - -

    Test private cli flag.

    - -
    - Source code in tests\test_cli.py -
    85
    -86
    -87
    -88
    -89
    -90
    -91
    -92
    -93
    -94
    def test_cli_private(folder):
    -    """
    -    Test private cli flag.
    -    """
    -    folder, torrent = folder
    -    args = ["torrentfile", "create", folder, "--private"]
    -    sys.argv = args
    -    execute()
    -    meta = pyben.load(torrent)
    -    assert "private" in meta["info"]
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_slash_outpath(dir1, sep) - - -

    - - -
    - -

    Test if output when outpath ends with a /.

    - -
    - Source code in tests\test_cli.py -
    426
    -427
    -428
    -429
    -430
    -431
    -432
    -433
    -434
    -435
    -436
    -437
    -438
    -439
    -440
    -441
    -442
    -443
    -444
    -445
    -446
    -447
    -448
    @pytest.mark.parametrize("sep", ["/", "\\"])
    -def test_cli_slash_outpath(dir1, sep):
    -    """
    -    Test if output when outpath ends with a /.
    -    """
    -    if sys.platform != "win32":
    -        sep = "/"  # pragma: nocover
    -    parent = os.path.dirname(dir1) + sep
    -    args = [
    -        "torrentfile",
    -        "create",
    -        "-t",
    -        "https://announce1.org",
    -        "--private",
    -        "-o",
    -        parent,
    -        str(dir1),
    -    ]
    -    sys.argv = args
    -    execute()
    -    outfile = str(dir1) + ".torrent"
    -    assert os.path.exists(outfile)
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_slash_path(dir1, ending) - - -

    - - -
    - -

    Test if output when path ends with a /.

    - -
    - Source code in tests\test_cli.py -
    404
    -405
    -406
    -407
    -408
    -409
    -410
    -411
    -412
    -413
    -414
    -415
    -416
    -417
    -418
    -419
    -420
    -421
    -422
    -423
    @pytest.mark.parametrize("ending", ["/", "\\"])
    -def test_cli_slash_path(dir1, ending):
    -    """
    -    Test if output when path ends with a /.
    -    """
    -    if sys.platform != "win32" and ending == "\\":  # pragma: nocover
    -        ending = "/"
    -    args = [
    -        "torrentfile",
    -        "create",
    -        "-t",
    -        "https://announce1.org",
    -        "--private",
    -        str(dir1) + ending,
    -    ]
    -    sys.argv = args
    -    execute()
    -    outfile = str(dir1) + ".torrent"
    -    assert os.path.exists(outfile)
    -    rmpath(outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_v1(folder) - - -

    - - -
    - -

    Basic create torrent cli command.

    - -
    - Source code in tests\test_cli.py -
    52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    def test_cli_v1(folder):
    -    """
    -    Basic create torrent cli command.
    -    """
    -    folder, torrent = folder
    -    args = ["torrentfile", "create", folder]
    -    sys.argv = args
    -    execute()
    -    assert os.path.exists(torrent)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_v2(folder) - - -

    - - -
    - -

    Create torrent v2 cli command.

    - -
    - Source code in tests\test_cli.py -
    63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    def test_cli_v2(folder):
    -    """
    -    Create torrent v2 cli command.
    -    """
    -    folder, torrent = folder
    -    args = ["torrentfile", "create", folder, "--meta-version", "2"]
    -    sys.argv = args
    -    execute()
    -    assert os.path.exists(torrent)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_v3(folder) - - -

    - - -
    - -

    Create hybrid torrent cli command.

    - -
    - Source code in tests\test_cli.py -
    74
    -75
    -76
    -77
    -78
    -79
    -80
    -81
    -82
    def test_cli_v3(folder):
    -    """
    -    Create hybrid torrent cli command.
    -    """
    -    folder, torrent = folder
    -    args = ["torrentfile", "create", folder, "--meta-version", "3"]
    -    sys.argv = args
    -    execute()
    -    assert os.path.exists(torrent)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_web_seeds(folder, piece_length, version) - - -

    - - -
    - -

    Test if created torrents recieve a web seeds field in meta info.

    - -
    - Source code in tests\test_cli.py -
    278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    -295
    -296
    -297
    -298
    -299
    -300
    -301
    @pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
    -@pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_web_seeds(folder, piece_length, version):
    -    """
    -    Test if created torrents recieve a web seeds field in meta info.
    -    """
    -    folder, torrent = folder
    -    args = [
    -        "torrentfile",
    -        "create",
    -        folder,
    -        "--piece-length",
    -        str(piece_length),
    -        "--meta-version",
    -        version,
    -        "-w",
    -        "https://webseed.url/1",
    -        "https://webseed.url/2",
    -        "https://webseed.url/3",
    -    ]
    -    sys.argv = args
    -    execute()
    -    meta = pyben.load(torrent)
    -    assert "https://webseed.url/1" in meta["url-list"]
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_with_debug(folder, piece_length, version) - - -

    - - -
    - -

    Test debug mode cli flag.

    - -
    - Source code in tests\test_cli.py -
    304
    -305
    -306
    -307
    -308
    -309
    -310
    -311
    -312
    -313
    -314
    -315
    -316
    -317
    -318
    -319
    -320
    -321
    -322
    -323
    -324
    -325
    @pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
    -@pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_with_debug(folder, piece_length, version):
    -    """
    -    Test debug mode cli flag.
    -    """
    -    folder, torrent = folder
    -    args = [
    -        "torrentfile",
    -        "-v",
    -        "create",
    -        folder,
    -        "--piece-length",
    -        str(piece_length),
    -        "--meta-version",
    -        version,
    -        "--comment",
    -        "this is a comment",
    -    ]
    -    sys.argv = args
    -    execute()
    -    assert os.path.exists(torrent)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_cli_with_source(folder, piece_length, version) - - -

    - - -
    - -

    Test source cli flag.

    - -
    - Source code in tests\test_cli.py -
    328
    -329
    -330
    -331
    -332
    -333
    -334
    -335
    -336
    -337
    -338
    -339
    -340
    -341
    -342
    -343
    -344
    -345
    -346
    -347
    -348
    -349
    @pytest.mark.parametrize("piece_length", [2**exp for exp in range(14, 21)])
    -@pytest.mark.parametrize("version", ["1", "2", "3"])
    -def test_cli_with_source(folder, piece_length, version):
    -    """
    -    Test source cli flag.
    -    """
    -    folder, torrent = folder
    -    args = [
    -        "torrentfile",
    -        "create",
    -        folder,
    -        "--piece-length",
    -        str(piece_length),
    -        "--meta-version",
    -        version,
    -        "--source",
    -        "somesource",
    -    ]
    -    sys.argv = args
    -    execute()
    -    meta = pyben.load(torrent)
    -    assert meta["info"]["source"] == "somesource"
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_fix() - - -

    - - -
    - -

    Test dir1 fixture is not None.

    - -
    - Source code in tests\test_cli.py -
    34
    -35
    -36
    -37
    -38
    def test_fix():
    -    """
    -    Test dir1 fixture is not None.
    -    """
    -    assert dir1 and dir2
    -
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - test_commands - - - -

    - -
    - -

    Testing functions for the sub-action commands from command line args.

    - - - -
    - - - - - - - - -
    - - - -

    -test_create_unicode_name(file1) - - -

    - - -
    - -

    Test Unicode information in CLI args.

    - -
    - Source code in tests\test_commands.py -
    145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    def test_create_unicode_name(file1):
    -    """
    -    Test Unicode information in CLI args.
    -    """
    -    parent = os.path.dirname(file1)
    -    filename = os.path.join(parent, "丂七万丈三与丏丑丒专且丕世丗両丢丣两严丩个丫丬中丮丯.torrent")
    -    args = [
    -        "torrentfile",
    -        "-v",
    -        "create",
    -        "-a",
    -        "tracker_url.com/announce_3456",
    -        "tracker_url.net/announce_3456",
    -        "--source",
    -        "sourcetext",
    -        "--comment",
    -        "filename is 丂七万丈三与丏丑丒专且丕世丗両丢丣两严丩个丫丬中丮丯.torrent",
    -        "-o",
    -        str(filename),
    -        str(file1),
    -    ]
    -    sys.argv = args
    -    execute()
    -    assert os.path.exists(filename)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_fix() - - -

    - - -
    - -

    Test dir1 fixture is not None.

    - -
    - Source code in tests\test_commands.py -
    37
    -38
    -39
    -40
    -41
    def test_fix():
    -    """
    -    Test dir1 fixture is not None.
    -    """
    -    assert dir1 and metafile1 and file1 and metafile2 and dir2
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_info(field, file1) - - -

    - - -
    - -

    Test the info_command action from the Command Line Interface.

    - -
    - Source code in tests\test_commands.py -
     94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    @pytest.mark.parametrize(
    -    "field",
    -    ["name", "announce", "source", "comment", "private", "announce-list"],
    -)
    -def test_info(field, file1):
    -    """
    -    Test the info_command action from the Command Line Interface.
    -    """
    -    args = [
    -        "torrentfile",
    -        "create",
    -        "-t",
    -        "url1",
    -        "url2",
    -        "url3",
    -        "--web-seed",
    -        "url4",
    -        "url5",
    -        "--http-seed",
    -        "url6",
    -        "url7",
    -        "--private",
    -        "--comment",
    -        "ExampleComment",
    -        "--source",
    -        "examplesource",
    -        str(file1),
    -    ]
    -    sys.argv = args
    -    execute()
    -
    -    class Space:
    -        """
    -        Stand in substitution for argparse.Namespace object.
    -        """
    -
    -        metafile = str(file1) + ".torrent"
    -
    -    output = info(Space)
    -    assert field in output
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_magnet(metafile1) - - -

    - - -
    - -

    Test create magnet function scheme.

    - -
    - Source code in tests\test_commands.py -
    65
    -66
    -67
    -68
    -69
    -70
    def test_magnet(metafile1):
    -    """
    -    Test create magnet function scheme.
    -    """
    -    magnet_link = magnet(metafile1)
    -    assert magnet_link.startswith("magnet")
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_magnet_cli(metafile1) - - -

    - - -
    - -

    Test magnet creation through CLI interface.

    - -
    - Source code in tests\test_commands.py -
    136
    -137
    -138
    -139
    -140
    -141
    -142
    def test_magnet_cli(metafile1):
    -    """
    -    Test magnet creation through CLI interface.
    -    """
    -    sys.argv[1:] = ["m", str(metafile1)]
    -    uri = execute()
    -    assert "magnet" in uri
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_magnet_empty() - - -

    - - -
    - -

    Test create magnet function scheme.

    - -
    - Source code in tests\test_commands.py -
    84
    -85
    -86
    -87
    -88
    -89
    -90
    -91
    def test_magnet_empty():
    -    """
    -    Test create magnet function scheme.
    -    """
    -    try:
    -        magnet("file_that_does_not_exist")
    -    except FileNotFoundError:
    -        assert True
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_magnet_hex(metafile1) - - -

    - - -
    - -

    Test create magnet function digest.

    - -
    - Source code in tests\test_commands.py -
    54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    def test_magnet_hex(metafile1):
    -    """
    -    Test create magnet function digest.
    -    """
    -    magnet_link = magnet(metafile1)
    -    meta = pyben.load(metafile1)
    -    info = meta["info"]
    -    binfo = sha1(pyben.dumps(info)).hexdigest().upper()
    -    assert binfo in magnet_link
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_magnet_no_announce_list(metafile2) - - -

    - - -
    - -

    Test create magnet function scheme.

    - -
    - Source code in tests\test_commands.py -
    73
    -74
    -75
    -76
    -77
    -78
    -79
    -80
    -81
    def test_magnet_no_announce_list(metafile2):
    -    """
    -    Test create magnet function scheme.
    -    """
    -    meta = pyben.load(metafile2)
    -    del meta["announce-list"]
    -    pyben.dump(meta, metafile2)
    -    magnet_link = magnet(metafile2)
    -    assert magnet_link.startswith("magnet")
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_magnet_uri(metafile1) - - -

    - - -
    - -

    Test create magnet function digest.

    - -
    - Source code in tests\test_commands.py -
    44
    -45
    -46
    -47
    -48
    -49
    -50
    -51
    def test_magnet_uri(metafile1):
    -    """
    -    Test create magnet function digest.
    -    """
    -    magnet_link = magnet(metafile1)
    -    meta = pyben.load(metafile1)
    -    announce = meta["announce"]
    -    assert quote_plus(announce) in magnet_link
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_merkle_root_no_blocks(blocks) - - -

    - - -
    - -

    Test running merkle root function with 1 and 0 len lists.

    - -
    - Source code in tests\test_commands.py -
    171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    @pytest.mark.parametrize("blocks", [[], [sha1(b"1010").digest()]])  # nosec
    -def test_merkle_root_no_blocks(blocks):
    -    """
    -    Test running merkle root function with 1 and 0 len lists.
    -    """
    -    if blocks:
    -        assert merkle_root(blocks)
    -    else:
    -        assert not merkle_root(blocks)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_mixins_progbar(torrent) - - -

    - - -
    - -

    Test progbar mixins with small file.

    - -
    - Source code in tests\test_commands.py -
    182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    @pytest.mark.parametrize("torrent", torrents())
    -def test_mixins_progbar(torrent):
    -    """
    -    Test progbar mixins with small file.
    -    """
    -    tfile = tempfile(exp=14)
    -    msg = "1234abcd" * 80
    -    with open(tfile, "wb") as temp:
    -        temp.write(msg.encode("utf-8"))
    -    args = {
    -        "path": tfile,
    -        "--prog": "1",
    -    }
    -    metafile = torrent(**args)
    -    output, _ = metafile.write()
    -    assert output == str(tfile) + ".torrent"
    -    rmpath(tfile)
    -
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - test_edit - - - -

    - -
    - -

    Testing the edit torrent feature.

    - - - -
    - - - - - - - - -
    - - - -

    -test_edit_cli(metafile2, comment, source, announce, webseed, httpseed) - - -

    - - -
    - -

    Test edit torrent with all params on cli.

    - -
    - Source code in tests\test_edit.py -
    198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    @pytest.mark.parametrize("comment", ["commenta", "commentb", "commentc"])
    -@pytest.mark.parametrize("source", ["sourcea", "sourceb", "sourcec"])
    -@pytest.mark.parametrize("announce", [["url1", "url2", "url3"], ["url1"]])
    -@pytest.mark.parametrize("webseed", [["ftp1"], ["ftpa", "ftpb"]])
    -@pytest.mark.parametrize("httpseed", [["ftp1"], ["ftpa", "ftpb"]])
    -def test_edit_cli(metafile2, comment, source, announce, webseed, httpseed):
    -    """
    -    Test edit torrent with all params on cli.
    -    """
    -    sys.argv = [
    -        "torrentfile",
    -        "edit",
    -        metafile2,
    -        "--comment",
    -        comment,
    -        "--source",
    -        source,
    -        "--web-seed",
    -        webseed,
    -        "--http-seed",
    -        httpseed,
    -        "--tracker",
    -        announce,
    -        "--private",
    -    ]
    -    main()
    -    meta = pyben.load(metafile2)
    -    info = meta["info"]
    -    assert comment == info.get("comment")
    -    assert source == info.get("source")
    -    assert info.get("private") == 1
    -    assert meta["announce-list"] == [[announce]]
    -    assert meta["url-list"] == [webseed]
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_comment(metafile2, comment) - - -

    - - -
    - -

    Test edit torrent with comment param.

    - -
    - Source code in tests\test_edit.py -
    118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    @pytest.mark.parametrize("comment", ["COMMENT", "COMIT", "MITCO"])
    -def test_edit_comment(metafile2, comment):
    -    """
    -    Test edit torrent with comment param.
    -    """
    -    edits = {"comment": comment}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert data["info"]["comment"] == comment
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_httpseeds(metafile2, httpseed) - - -

    - - -
    - -

    Test edit torrent with webseed param as string.

    - -
    - Source code in tests\test_edit.py -
    104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    @pytest.mark.parametrize(
    -    "httpseed", [["urla"], ["urlb", "urlc"], ["urla", "urlb", "urlc"]]
    -)
    -def test_edit_httpseeds(metafile2, httpseed):
    -    """
    -    Test edit torrent with webseed param as string.
    -    """
    -    edits = {"httpseeds": httpseed}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert data["httpseeds"] == httpseed
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_httpseeds_str(metafile2, httpseeds) - - -

    - - -
    - -

    Test edit torrent with webseed param.

    - -
    - Source code in tests\test_edit.py -
    78
    -79
    -80
    -81
    -82
    -83
    -84
    -85
    -86
    -87
    @pytest.mark.parametrize("httpseeds", ["urla", "urlb urlc", "urla urlb urlc"])
    -def test_edit_httpseeds_str(metafile2, httpseeds):
    -    """
    -    Test edit torrent with webseed param.
    -    """
    -    edits = {"httpseeds": httpseeds}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert data["httpseeds"] == httpseeds.split()
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_none(metafile2) - - -

    - - -
    - -

    Test edit torrent with None for all params.

    - -
    - Source code in tests\test_edit.py -
    164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    def test_edit_none(metafile2):
    -    """
    -    Test edit torrent with None for all params.
    -    """
    -    edits = {
    -        "announce": None,
    -        "url-list": None,
    -        "comment": None,
    -        "source": None,
    -        "private": None,
    -    }
    -    data = pyben.load(metafile2)
    -    edited = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta == edited
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_private_false(metafile2) - - -

    - - -
    - -

    Test edit torrent with private param False.

    - -
    - Source code in tests\test_edit.py -
    153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    def test_edit_private_false(metafile2):
    -    """
    -    Test edit torrent with private param False.
    -    """
    -    edits = {"private": ""}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert "private" not in data["info"]
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_private_true(metafile2) - - -

    - - -
    - -

    Test edit torrent with private param.

    - -
    - Source code in tests\test_edit.py -
    142
    -143
    -144
    -145
    -146
    -147
    -148
    -149
    -150
    def test_edit_private_true(metafile2):
    -    """
    -    Test edit torrent with private param.
    -    """
    -    edits = {"private": "1"}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert data["info"]["private"] == 1
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_removal(metafile2) - - -

    - - -
    - -

    Test edit torrent with empty for all params.

    - -
    - Source code in tests\test_edit.py -
    181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    def test_edit_removal(metafile2):
    -    """
    -    Test edit torrent with empty for all params.
    -    """
    -    edits = {
    -        "announce": "",
    -        "url-list": "",
    -        "httpseeds": "",
    -        "comment": "",
    -        "source": "",
    -        "private": "",
    -    }
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_source(metafile2, source) - - -

    - - -
    - -

    Test edit torrent with source param.

    - -
    - Source code in tests\test_edit.py -
    130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    @pytest.mark.parametrize("source", ["SomeSource", "NoSouce", "MidSource"])
    -def test_edit_source(metafile2, source):
    -    """
    -    Test edit torrent with source param.
    -    """
    -    edits = {"source": source}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert data["info"]["source"] == source
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_torrent(metafile2, announce) - - -

    - - -
    - -

    Test edit torrent with announce param.

    - -
    - Source code in tests\test_edit.py -
    40
    -41
    -42
    -43
    -44
    -45
    -46
    -47
    -48
    -49
    -50
    -51
    @pytest.mark.parametrize(
    -    "announce", [["urla"], ["urlb", "urlc"], ["urla", "urlb", "urlc"]]
    -)
    -def test_edit_torrent(metafile2, announce):
    -    """
    -    Test edit torrent with announce param.
    -    """
    -    edits = {"announce": announce}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert data["announce-list"] == [announce]
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_torrent_str(metafile2, announce) - - -

    - - -
    - -

    Test edit torrent with announce param as string.

    - -
    - Source code in tests\test_edit.py -
    54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    @pytest.mark.parametrize("announce", ["urla", "urlb urlc", "urla urlb urlc"])
    -def test_edit_torrent_str(metafile2, announce):
    -    """
    -    Test edit torrent with announce param as string.
    -    """
    -    edits = {"announce": announce}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert data["announce-list"] == [announce.split()]
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_urllist(metafile2, url_list) - - -

    - - -
    - -

    Test edit torrent with webseed param as string.

    - -
    - Source code in tests\test_edit.py -
     90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    @pytest.mark.parametrize(
    -    "url_list", [["urla"], ["urlb", "urlc"], ["urla", "urlb", "urlc"]]
    -)
    -def test_edit_urllist(metafile2, url_list):
    -    """
    -    Test edit torrent with webseed param as string.
    -    """
    -    edits = {"url-list": url_list}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert data["url-list"] == url_list
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_edit_urllist_str(metafile2, url_list) - - -

    - - -
    - -

    Test edit torrent with webseed param.

    - -
    - Source code in tests\test_edit.py -
    66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    @pytest.mark.parametrize("url_list", ["urla", "urlb urlc", "urla urlb urlc"])
    -def test_edit_urllist_str(metafile2, url_list):
    -    """
    -    Test edit torrent with webseed param.
    -    """
    -    edits = {"url-list": url_list}
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    assert data == meta
    -    assert data["url-list"] == url_list.split()
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_fix() - - -

    - - -
    - -

    Testing dir fixtures.

    - -
    - Source code in tests\test_edit.py -
    33
    -34
    -35
    -36
    -37
    def test_fix():
    -    """
    -    Testing dir fixtures.
    -    """
    -    assert dir2 and metafile2 and dir1
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_metafile_edit_with_unicode(metafile2) - - -

    - - -
    - -

    Test if editing full unicode works as it should.

    - -
    - Source code in tests\test_edit.py -
    233
    -234
    -235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    def test_metafile_edit_with_unicode(metafile2):
    -    """
    -    Test if editing full unicode works as it should.
    -    """
    -    edits = {
    -        "comment": "丂七万丈三与丏丑丒专且丕世丗両丢丣两严丩个丫丬中丮丯.torrent",
    -        "source": "丂七万丏丑严丩个丫丬中丮丯",
    -    }
    -    data = edit_torrent(metafile2, edits)
    -    meta = pyben.load(metafile2)
    -    com1 = data["info"]["comment"]
    -    com2 = meta["info"]["comment"]
    -    msg = edits["comment"]
    -    assert com1 == com2 == msg
    -
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - test_interactive - - - -

    - -
    - -

    Testing functions for the command line interface.

    - - - -
    - - - - - - - - - -
    - - - -

    -test_fixtures() - - -

    - - -
    - -

    Test the fixtures used in module.

    - -
    - Source code in tests\test_interactive.py -
    37
    -38
    -39
    -40
    -41
    def test_fixtures():
    -    """
    -    Test the fixtures used in module.
    -    """
    -    assert filemeta2 and file1 and file2
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_inter_create_full(file1, piece_length, announce, comment, source, url_list, version, monkeypatch) - - -

    - - -
    - -

    Test creating torrent interactively with many parameters.

    - -
    - Source code in tests\test_interactive.py -
     67
    - 68
    - 69
    - 70
    - 71
    - 72
    - 73
    - 74
    - 75
    - 76
    - 77
    - 78
    - 79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    -104
    -105
    -106
    @pytest.mark.parametrize("version", ["1", "2", "3"])
    -@pytest.mark.parametrize("piece_length", ["23", "18", "131072"])
    -@pytest.mark.parametrize("announce", ["url1", "urla urlb urlc"])
    -@pytest.mark.parametrize("url_list", ["ftp url2", "ftp1 ftp2 ftp3"])
    -@pytest.mark.parametrize("comment", ["Some Comment", "No Comment"])
    -@pytest.mark.parametrize("source", ["Do", "Ra", "Me"])
    -def test_inter_create_full(
    -    file1,
    -    piece_length,
    -    announce,
    -    comment,
    -    source,
    -    url_list,
    -    version,
    -    monkeypatch,
    -):
    -    """
    -    Test creating torrent interactively with many parameters.
    -    """
    -    mapping = [
    -        "create",
    -        piece_length,
    -        announce,
    -        url_list,
    -        url_list,
    -        comment,
    -        source,
    -        "Y",
    -        file1,
    -        str(file1) + ".torrent",
    -        version,
    -    ]
    -    it = iter(mapping)
    -    monkeypatch.setattr(MOCK, lambda *_: next(it))
    -    select_action()
    -    meta = pyben.load(str(file1) + ".torrent")
    -    assert meta["info"]["source"] == source
    -    assert meta["info"]["piece length"] == normalize_piece_length(piece_length)
    -    assert meta["info"]["comment"] == comment
    -    assert meta["url-list"] == url_list.split()
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_inter_edit_cli(filemeta2, announce, cmnt, srce, urllist, monkeypatch) - - -

    - - -
    - -

    Test editing torrent interactively from CLI.

    - -
    - Source code in tests\test_interactive.py -
    145
    -146
    -147
    -148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    @pytest.mark.parametrize("announce", ["urla urlb urlc", "urld url2"])
    -@pytest.mark.parametrize("urllist", ["ftp url2", "ftp1 ftp2 ftp3"])
    -@pytest.mark.parametrize("cmnt", ["Some Comment"])
    -@pytest.mark.parametrize("srce", ["Do", "Ra"])
    -def test_inter_edit_cli(filemeta2, announce, cmnt, srce, urllist, monkeypatch):
    -    """
    -    Test editing torrent interactively from CLI.
    -    """
    -    seq = [
    -        "edit",
    -        filemeta2,
    -        "4",
    -        announce,
    -        "1",
    -        cmnt,
    -        "2",
    -        srce,
    -        "5",
    -        urllist,
    -        urllist,
    -        "6",
    -        "Y",
    -        "DONE",
    -    ]
    -    it = iter(seq)
    -    monkeypatch.setattr(MOCK, lambda *_: next(it))
    -    sys.argv = ["torrentfile", "-i"]
    -    main()
    -    meta2 = pyben.load(filemeta2)
    -    assert meta2["info"]["source"] == srce
    -    assert meta2["info"]["comment"] == cmnt
    -    assert meta2["url-list"] == urllist.split()
    -    assert meta2["info"]["private"] == 1
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_inter_edit_full(filemeta2, announce, comment, source, url_list, monkeypatch) - - -

    - - -
    - -

    Test editing torrent file interactively.

    - -
    - Source code in tests\test_interactive.py -
    109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    -126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    @pytest.mark.parametrize("announce", ["url1"])
    -@pytest.mark.parametrize("url_list", ["ftp url2", "ftp1 ftp2 ftp3"])
    -@pytest.mark.parametrize("comment", ["Some Comment", "No Comment"])
    -@pytest.mark.parametrize("source", ["Fa", "So", "La"])
    -def test_inter_edit_full(
    -    filemeta2, announce, comment, source, url_list, monkeypatch
    -):
    -    """
    -    Test editing torrent file interactively.
    -    """
    -    seq = [
    -        "edit",
    -        filemeta2,
    -        "4",
    -        announce,
    -        "1",
    -        comment,
    -        "2",
    -        source,
    -        "5",
    -        url_list,
    -        "",
    -        "6",
    -        "Y",
    -        "DONE",
    -    ]
    -    it = iter(seq)
    -    monkeypatch.setattr(MOCK, lambda *_: next(it))
    -    select_action()
    -    meta1 = pyben.load(filemeta2)
    -    assert meta1["info"]["source"] == source
    -    assert meta1["info"]["comment"] == comment
    -    assert meta1["url-list"] == url_list.split()
    -    assert meta1["info"]["private"] == 1
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_inter_recheck(torrentclass, monkeypatch, file1) - - -

    - - -
    - -

    Test interactive recheck function.

    - -
    - Source code in tests\test_interactive.py -
    180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    @pytest.mark.parametrize("torrentclass", torrents())
    -def test_inter_recheck(torrentclass, monkeypatch, file1):
    -    """
    -    Test interactive recheck function.
    -    """
    -    torrent = torrentclass(path=file1)
    -    filemeta, _ = torrent.write()
    -    seq = ["recheck", filemeta, str(file1)]
    -    it = iter(seq)
    -    monkeypatch.setattr(MOCK, lambda *_: next(it))
    -    result = select_action()
    -    assert result == 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_interactive_create(monkeypatch, file1) - - -

    - - -
    - -

    Test creating torrent interactively.

    - -
    - Source code in tests\test_interactive.py -
    44
    -45
    -46
    -47
    -48
    -49
    -50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    def test_interactive_create(monkeypatch, file1):
    -    """
    -    Test creating torrent interactively.
    -    """
    -    mapping = [
    -        "create",
    -        "",
    -        "",
    -        "",
    -        "",
    -        "",
    -        "",
    -        "",
    -        file1,
    -        str(file1) + ".torrent",
    -        "",
    -    ]
    -    it = iter(mapping)
    -    monkeypatch.setattr(MOCK, lambda *_: next(it))
    -    select_action()
    -    assert os.path.exists(str(file1) + ".torrent")
    -
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - test_recheck - - - -

    - -
    - -

    Testing functions for the progress module.

    - - - -
    - - - - - - - - -
    - - - -

    -test_checker_callback(dir1, metafile1) - - -

    - - -
    - -

    Test Checker class with directory that points to nothing.

    - -
    - Source code in tests\test_recheck.py -
    125
    -126
    -127
    -128
    -129
    -130
    -131
    def test_checker_callback(dir1, metafile1):
    -    """
    -    Test Checker class with directory that points to nothing.
    -    """
    -    Checker.register_callback(lambda *x: print(x))
    -    checker = Checker(metafile1, str(dir1))
    -    assert checker.results() == 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_class(dir1, metafile1) - - -

    - - -
    - -

    Test Checker Class against meta files.

    - -
    - Source code in tests\test_recheck.py -
    42
    -43
    -44
    -45
    -46
    -47
    def test_checker_class(dir1, metafile1):
    -    """
    -    Test Checker Class against meta files.
    -    """
    -    checker = Checker(metafile1, dir1)
    -    assert checker.results() == 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_class_allfiles(sizedfiles, dir2) - - -

    - - -
    - -

    Test Checker class when all files are missing from contents.

    - -
    - Source code in tests\test_recheck.py -
    191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    def test_checker_class_allfiles(sizedfiles, dir2):
    -    """
    -    Test Checker class when all files are missing from contents.
    -    """
    -
    -    def traverse(path):
    -        """
    -        Traverse internal subdirectories.
    -        """
    -        if path.is_file():
    -            rmpath(path)
    -        elif path.is_dir():
    -            for item in path.iterdir():
    -                traverse(item)
    -
    -    traverse(dir2)
    -    checker = Checker(sizedfiles, dir2)
    -    assert int(checker.results()) < 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_class_allpaths(sizedfiles, dir2) - - -

    - - -
    - -

    Test Checker class when all files are missing from contents.

    - -
    - Source code in tests\test_recheck.py -
    211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    def test_checker_class_allpaths(sizedfiles, dir2):
    -    """
    -    Test Checker class when all files are missing from contents.
    -    """
    -    for item in Path(str(dir2)).iterdir():
    -        rmpath(item)
    -    checker = Checker(sizedfiles, dir2)
    -    assert int(checker.results()) < 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_class_half_file(filemeta2, file2) - - -

    - - -
    - -

    Test Checker class with half size single file.

    - -
    - Source code in tests\test_recheck.py -
    221
    -222
    -223
    -224
    -225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    def test_checker_class_half_file(filemeta2, file2):
    -    """
    -    Test Checker class with half size single file.
    -    """
    -    half = int(os.path.getsize(file2) / 2)
    -    barr = bytearray(half)
    -    with open(file2, "rb") as content:
    -        content.readinto(barr)
    -    with open(file2, "wb") as content:
    -        content.write(barr)
    -    checker = Checker(filemeta2, file2)
    -    assert int(checker.results()) != 10
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_cli_args(dir1, metafile1) - - -

    - - -
    - -

    Test exclusive Checker Mode CLI.

    - -
    - Source code in tests\test_recheck.py -
    134
    -135
    -136
    -137
    -138
    -139
    -140
    def test_checker_cli_args(dir1, metafile1):
    -    """
    -    Test exclusive Checker Mode CLI.
    -    """
    -    sys.argv = ["torrentfile", "check", str(metafile1), str(dir1)]
    -    output = main()
    -    assert output == 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_empty_files(dir2, sizedfiles) - - -

    - - -
    - -

    Test Checker when directory contains 0 length files.

    - -
    - Source code in tests\test_recheck.py -
    274
    -275
    -276
    -277
    -278
    -279
    -280
    -281
    -282
    -283
    -284
    -285
    -286
    -287
    -288
    -289
    -290
    -291
    -292
    -293
    -294
    def test_checker_empty_files(dir2, sizedfiles):
    -    """
    -    Test Checker when directory contains 0 length files.
    -    """
    -
    -    def empty_files(root):
    -        """
    -        Dump contents of files.
    -        """
    -        if os.path.isfile(root):
    -            with open(root, "wb") as _:
    -                pass
    -            assert os.path.getsize(root) == 0
    -        elif os.path.isdir(root):
    -            for item in os.listdir(root):
    -                return empty_files(os.path.join(root, item))
    -        return root
    -
    -    empty_files(dir2)
    -    checker = Checker(sizedfiles, dir2)
    -    assert checker.results() != 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_first_piece(dir2, sizedfiles) - - -

    - - -
    - -

    Test Checker Class when first piece is slightly alterred.

    - -
    - Source code in tests\test_recheck.py -
    50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    -59
    -60
    -61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    def test_checker_first_piece(dir2, sizedfiles):
    -    """
    -    Test Checker Class when first piece is slightly alterred.
    -    """
    -
    -    def change(path):
    -        """
    -        Change some bytes in file.
    -        """
    -        if path.is_file():
    -            new = b"Something other than what was there before."
    -            with open(path, "rb") as bfile:
    -                data = bfile.read()
    -            new_len = len(new)
    -            content = b"".join([new, data[new_len:]])
    -            with open(path, "wb") as bdoc:
    -                bdoc.write(content)
    -        elif path.is_dir():
    -            for item in path.iterdir():
    -                change(item)
    -
    -    change(Path(dir2))
    -    checker = Checker(sizedfiles, dir2)
    -    assert checker.results() != 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_first_piece_alt(dir2, sizedfiles) - - -

    - - -
    - -

    Test Checker Class when first piece is slightly alterred.

    - -
    - Source code in tests\test_recheck.py -
    76
    -77
    -78
    -79
    -80
    -81
    -82
    -83
    -84
    -85
    -86
    -87
    -88
    -89
    -90
    -91
    -92
    -93
    -94
    -95
    -96
    -97
    -98
    def test_checker_first_piece_alt(dir2, sizedfiles):
    -    """
    -    Test Checker Class when first piece is slightly alterred.
    -    """
    -
    -    def change(path):
    -        """
    -        Change some bytes in file.
    -        """
    -        if os.path.isfile(path):
    -            with open(path, "rb") as bfile:
    -                data = bfile.read()
    -            new = b"some_other_bytes_to_use"
    -            new_len = len(new)
    -            with open(path, "wb") as wfile:
    -                wfile.write(new + data[new_len:])
    -        elif os.path.isdir(path):
    -            for item in os.listdir(path):
    -                change(os.path.join(path, item))
    -
    -    change(dir2)
    -    checker = Checker(sizedfiles, dir2)
    -    assert checker.results() != 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_missing(sizedfiles, dir2) - - -

    - - -
    - -

    Test Checker class when files are missing from contents.

    - -
    - Source code in tests\test_recheck.py -
    179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    def test_checker_missing(sizedfiles, dir2):
    -    """
    -    Test Checker class when files are missing from contents.
    -    """
    -    count = 0
    -    for fd in Path(dir2).iterdir():
    -        if fd.is_file() and count < 2:
    -            rmpath(fd)
    -    checker = Checker(sizedfiles, dir2)
    -    assert int(checker.results()) < 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_missing_singles(dir2, sizedfiles) - - -

    - - -
    - -

    Test Checker class with half size single file.

    - -
    - Source code in tests\test_recheck.py -
    235
    -236
    -237
    -238
    -239
    -240
    -241
    -242
    -243
    -244
    -245
    -246
    -247
    -248
    -249
    -250
    -251
    -252
    -253
    -254
    def test_checker_missing_singles(dir2, sizedfiles):
    -    """
    -    Test Checker class with half size single file.
    -    """
    -
    -    def walk(root):
    -        """
    -        Remove first file found.
    -        """
    -        if root.is_file():
    -            rmpath(root)
    -            return True
    -        if root.is_dir():
    -            for item in root.iterdir():
    -                walk(item)
    -        return False
    -
    -    walk(Path(dir2))
    -    checker = Checker(sizedfiles, dir2)
    -    assert int(checker.results()) < 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_no_meta_file() - - -

    - - -
    - -

    Test Checker when incorrect metafile is provided.

    - -
    - Source code in tests\test_recheck.py -
    159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    def test_checker_no_meta_file():
    -    """
    -    Test Checker when incorrect metafile is provided.
    -    """
    -    try:
    -        Checker("peaches", "$")
    -    except FileNotFoundError:
    -        assert True
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_parent_dir(dir1, metafile1) - - -

    - - -
    - -

    Test providing the parent directory for torrent checking feature.

    - -
    - Source code in tests\test_recheck.py -
    143
    -144
    -145
    -146
    -147
    -148
    def test_checker_parent_dir(dir1, metafile1):
    -    """
    -    Test providing the parent directory for torrent checking feature.
    -    """
    -    checker = Checker(metafile1, os.path.dirname(dir1))
    -    assert checker.results() == 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_result_property(dir1, metafile1) - - -

    - - -
    - -

    Test Checker class with half size single file.

    - -
    - Source code in tests\test_recheck.py -
    257
    -258
    -259
    -260
    -261
    -262
    -263
    def test_checker_result_property(dir1, metafile1):
    -    """
    -    Test Checker class with half size single file.
    -    """
    -    checker = Checker(metafile1, dir1)
    -    result = checker.results()
    -    assert checker.results() == result
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_simplest(dir1, metafile1) - - -

    - - -
    - -

    Test the simplest example.

    - -
    - Source code in tests\test_recheck.py -
    266
    -267
    -268
    -269
    -270
    -271
    def test_checker_simplest(dir1, metafile1):
    -    """
    -    Test the simplest example.
    -    """
    -    checker = Checker(metafile1, dir1)
    -    assert checker.results() == 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_with_file(file1, filemeta1) - - -

    - - -
    - -

    Test checker with single file torrent.

    - -
    - Source code in tests\test_recheck.py -
    151
    -152
    -153
    -154
    -155
    -156
    def test_checker_with_file(file1, filemeta1):
    -    """
    -    Test checker with single file torrent.
    -    """
    -    checker = Checker(filemeta1, file1)
    -    assert checker.results() == 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_checker_wrong_root_dir(metafile1) - - -

    - - -
    - -

    Test Checker when incorrect root directory is provided.

    - -
    - Source code in tests\test_recheck.py -
    169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    def test_checker_wrong_root_dir(metafile1):
    -    """
    -    Test Checker when incorrect root directory is provided.
    -    """
    -    try:
    -        Checker(metafile1, "fake")
    -    except FileNotFoundError:
    -        assert True
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_fixtures() - - -

    - - -
    - -

    Test fixtures exist.

    - -
    - Source code in tests\test_recheck.py -
    33
    -34
    -35
    -36
    -37
    -38
    -39
    def test_fixtures():
    -    """
    -    Test fixtures exist.
    -    """
    -    assert dir1 and dir2 and file1 and file2
    -    assert filemeta1 and filemeta2 and metafile1
    -    assert metafile2 and sizes and sizedfiles
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_partial_metafiles(dir2, sizedfiles) - - -

    - - -
    - -

    Test Checker with data that is expected to be incomplete.

    - -
    - Source code in tests\test_recheck.py -
    101
    -102
    -103
    -104
    -105
    -106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    def test_partial_metafiles(dir2, sizedfiles):
    -    """
    -    Test Checker with data that is expected to be incomplete.
    -    """
    -
    -    def shortenfile(path):
    -        """
    -        Shorten a few files for testing purposes.
    -        """
    -        with open(path, "rb") as bfile:
    -            data = bfile.read()
    -        with open(path, "wb") as bfile:
    -            bfile.write(data[: -(2**10)])
    -
    -    for item in os.listdir(dir2):
    -        full = os.path.join(dir2, item)
    -        if os.path.isfile(full):
    -            shortenfile(full)
    -
    -    testdir = os.path.dirname(dir2)
    -    checker = Checker(sizedfiles, testdir)
    -    assert checker.results() != 100
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_recheck_wrong_dir(metafile1) - - -

    - - -
    - -

    Test recheck function with directory that doesn't contain the contents.

    - -
    - Source code in tests\test_recheck.py -
    297
    -298
    -299
    -300
    -301
    -302
    -303
    -304
    -305
    def test_recheck_wrong_dir(metafile1):
    -    """
    -    Test recheck function with directory that doesn't contain the contents.
    -    """
    -    grandparent = os.path.dirname(os.path.dirname(metafile1))
    -    try:
    -        _ = Checker(metafile1, grandparent)
    -    except FileNotFoundError:
    -        assert True
    -
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - test_torrent - - - -

    - -
    - -

    Testing functions for the torrent module.

    - - - -
    - - - - - - - - -
    - - - -

    -test_create_cwd_fail() - - -

    - - -
    - -

    Test cwd argument with create command failure.

    - -
    - Source code in tests\test_torrent.py -
    175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    def test_create_cwd_fail():
    -    """Test cwd argument with create command failure."""
    -
    -    class SuFile:
    -        """A mock admin file."""
    -
    -        @staticmethod
    -        def __fspath__():
    -            raise PermissionError
    -
    -        def __str__(self):
    -            return "SuFile"
    -
    -    tfile = tempfile()
    -    name = os.path.basename(tfile) + ".torrent"
    -    torrent = MetaFile(path=tfile)
    -    sufile = SuFile()
    -    torrent.write(outfile=sufile)
    -    current = os.path.join(".", name)
    -    assert os.path.exists(current)
    -    rmpath(tfile, current)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_fixtures() - - -

    - - -
    - -

    Test pytest fixtures.

    - -
    - Source code in tests\test_torrent.py -
    32
    -33
    -34
    -35
    -36
    def test_fixtures():
    -    """
    -    Test pytest fixtures.
    -    """
    -    assert dir1 and dir2
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_mbtorrent(version, progress) - - -

    - - -
    - -

    Test torrent creation for file size larger than 10MB.

    - -
    - Source code in tests\test_torrent.py -
    209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    -223
    -224
    @pytest.mark.parametrize("version", torrents())
    -@pytest.mark.parametrize("progress", [0, 1, 2])
    -def test_mbtorrent(version, progress):
    -    """
    -    Test torrent creation for file size larger than 10MB.
    -    """
    -    tfile = tempfile(exp=26)
    -    args = {
    -        "path": tfile,
    -        "progress": progress,
    -        "piece_length": "14",
    -    }
    -    torrent = version(**args)
    -    outfile, _ = torrent.write()
    -    assert os.path.exists(outfile)
    -    rmpath(tfile, outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_metafile_assemble(dir1) - - -

    - - -
    - -

    Test assembling base metafile exception.

    - -
    - Source code in tests\test_torrent.py -
    50
    -51
    -52
    -53
    -54
    -55
    -56
    -57
    -58
    def test_metafile_assemble(dir1):
    -    """
    -    Test assembling base metafile exception.
    -    """
    -    metafile = MetaFile(path=dir1)
    -    try:
    -        metafile.assemble()
    -    except NotImplementedError:
    -        assert True
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_torrentfile_extra(dir2, version) - - -

    - - -
    - -

    Test creating a torrent meta file with given directory plus extra.

    - -
    - Source code in tests\test_torrent.py -
     79
    - 80
    - 81
    - 82
    - 83
    - 84
    - 85
    - 86
    - 87
    - 88
    - 89
    - 90
    - 91
    - 92
    - 93
    - 94
    - 95
    - 96
    - 97
    - 98
    - 99
    -100
    -101
    -102
    -103
    @pytest.mark.parametrize("version", torrents())
    -def test_torrentfile_extra(dir2, version):
    -    """
    -    Test creating a torrent meta file with given directory plus extra.
    -    """
    -
    -    def walk(item):
    -        """
    -        Edit files in directory structure.
    -        """
    -        if item.is_file():
    -            with open(item, "ab") as binfile:
    -                binfile.write(bytes(1000))
    -        elif item.is_dir():
    -            for sub in item.iterdir():
    -                walk(sub)
    -
    -    walk(dir2)
    -    args = {
    -        "path": dir2,
    -        "comment": "somecomment",
    -        "announce": "announce",
    -    }
    -    torrent = version(**args)
    -    assert torrent.meta["announce"] == "announce"
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_torrentfile_missing_path(version) - - -

    - - -
    - -

    Test missing path error exception.

    - -
    - Source code in tests\test_torrent.py -
    39
    -40
    -41
    -42
    -43
    -44
    -45
    -46
    -47
    @pytest.mark.parametrize("version", torrents())
    -def test_torrentfile_missing_path(version):
    -    """
    -    Test missing path error exception.
    -    """
    -    try:
    -        version()
    -    except MissingPathError:
    -        assert True
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_torrentfile_one_empty(dir2, version) - - -

    - - -
    - -

    Test creating a torrent meta file with given directory plus extra.

    - -
    - Source code in tests\test_torrent.py -
    61
    -62
    -63
    -64
    -65
    -66
    -67
    -68
    -69
    -70
    -71
    -72
    -73
    -74
    -75
    -76
    @pytest.mark.parametrize("version", torrents())
    -def test_torrentfile_one_empty(dir2, version):
    -    """
    -    Test creating a torrent meta file with given directory plus extra.
    -    """
    -    a = next(os.walk(dir2))
    -    if len(a[-1]) > 0:
    -        with open(os.path.join(a[0], a[-1][0]), "w", encoding="utf-8") as _:
    -            pass
    -    args = {
    -        "path": dir2,
    -        "comment": "somecomment",
    -        "announce": "announce",
    -    }
    -    torrent = version(**args)
    -    assert torrent.meta["announce"] == "announce"
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_torrentfile_single(version, num, piece_length, capsys) - - -

    - - -
    - -

    Test creating a torrent file from a single file contents.

    - -
    - Source code in tests\test_torrent.py -
    106
    -107
    -108
    -109
    -110
    -111
    -112
    -113
    -114
    -115
    -116
    -117
    -118
    -119
    -120
    -121
    -122
    -123
    -124
    -125
    @pytest.mark.parametrize("num", list(range(17, 25)))
    -@pytest.mark.parametrize("piece_length", [2**i for i in range(14, 18)])
    -@pytest.mark.parametrize("version", torrents())
    -def test_torrentfile_single(version, num, piece_length, capsys):
    -    """
    -    Test creating a torrent file from a single file contents.
    -    """
    -    tfile = tempfile(exp=num)
    -    with capsys.disabled():
    -        version.set_callback(print)
    -    args = {
    -        "path": tfile,
    -        "comment": "somecomment",
    -        "announce": "announce",
    -        "piece_length": piece_length,
    -    }
    -    trent = version(**args)
    -    trent.write()
    -    assert os.path.exists(str(tfile) + ".torrent")
    -    rmpath(tfile, str(tfile) + ".torrent")
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_torrentfile_single_extra(version, size, piece_length) - - -

    - - -
    - -

    Test creating a torrent file from a single file contents plus extra.

    - -
    - Source code in tests\test_torrent.py -
    128
    -129
    -130
    -131
    -132
    -133
    -134
    -135
    -136
    -137
    -138
    -139
    -140
    -141
    -142
    -143
    -144
    -145
    -146
    -147
    -148
    @pytest.mark.parametrize("size", list(range(17, 25)))
    -@pytest.mark.parametrize("piece_length", [2**i for i in range(14, 18)])
    -@pytest.mark.parametrize("version", torrents())
    -def test_torrentfile_single_extra(version, size, piece_length):
    -    """
    -    Test creating a torrent file from a single file contents plus extra.
    -    """
    -    tfile = tempfile(exp=size)
    -    with open(tfile, "ab") as binfile:
    -        binfile.write(bytes(str(tfile).encode("utf-8")))
    -    args = {
    -        "path": tfile,
    -        "comment": "somecomment",
    -        "announce": "announce",
    -        "piece_length": piece_length,
    -    }
    -    torrent = version(**args)
    -    torrent.write()
    -    outfile = str(tfile) + ".torrent"
    -    assert os.path.exists(outfile)
    -    rmpath(tfile, outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_torrentfile_single_under(ver, sze, piecelength) - - -

    - - -
    - -

    Test creating a torrent file from less than a single file contents.

    - -
    - Source code in tests\test_torrent.py -
    151
    -152
    -153
    -154
    -155
    -156
    -157
    -158
    -159
    -160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    @pytest.mark.parametrize("sze", list(range(17, 25)))
    -@pytest.mark.parametrize("piecelength", [2**i for i in range(14, 18)])
    -@pytest.mark.parametrize("ver", torrents())
    -def test_torrentfile_single_under(ver, sze, piecelength):
    -    """
    -    Test creating a torrent file from less than a single file contents.
    -    """
    -    tfile = tempfile(exp=sze)
    -    with open(tfile, "rb") as binfile:
    -        data = binfile.read()
    -    with open(tfile, "wb") as binfile:
    -        binfile.write(data[: -(2**9)])
    -    kwargs = {
    -        "path": tfile,
    -        "comment": "somecomment",
    -        "announce": "announce",
    -        "piece_length": piecelength,
    -    }
    -    torrent = ver(**kwargs)
    -    outfile, _ = torrent.write()
    -    assert os.path.exists(outfile)
    -    rmpath(tfile, outfile)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_waiting_mixin() - - -

    - - -
    - -

    Test waiting function.

    - -
    - Source code in tests\test_torrent.py -
    198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    def test_waiting_mixin():
    -    """
    -    Test waiting function.
    -    """
    -    msg = "Testing message"
    -    lst = []
    -    timeout = 3
    -    waiting(msg, lst, timeout=timeout)
    -    assert len(lst) == 0
    -
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    - - - -

    - test_utils - - - -

    - -
    - -

    Unittest functions for testing torrentfile utils module.

    - - - -
    - - - - - - - - -
    - - - -

    -test_filelist_total(dir1) - - -

    - - -
    - -

    Test function for acquiring a filelist for directory.

    - -
    - Source code in tests\test_utils.py -
    118
    -119
    -120
    -121
    -122
    -123
    def test_filelist_total(dir1):
    -    """
    -    Test function for acquiring a filelist for directory.
    -    """
    -    total, _ = utils.filelist_total(dir1)
    -    assert total == (2**18) * 8
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_filelisttotal_missing(dir2) - - -

    - - -
    - -

    Test function filelist total with missing path.

    - -

    Parameters:

    - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionDefault
    dir2 - pytest.fixture -

    fixture containing a temporary directory

    - required -
    - -
    - Source code in tests\test_utils.py -
    225
    -226
    -227
    -228
    -229
    -230
    -231
    -232
    -233
    -234
    -235
    -236
    -237
    def test_filelisttotal_missing(dir2):
    -    """Test function filelist total with missing path.
    -
    -    Parameters
    -    ----------
    -    dir2 : pytest.fixture
    -        fixture containing a temporary directory
    -    """
    -    rmpath(dir2)
    -    try:
    -        utils.filelist_total(dir2)
    -    except utils.MissingPathError:
    -        assert True
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_get_filelist(dir1) - - -

    - - -
    - -

    Test function for get a list of files in a directory.

    - -
    - Source code in tests\test_utils.py -
    102
    -103
    -104
    -105
    -106
    -107
    def test_get_filelist(dir1):
    -    """
    -    Test function for get a list of files in a directory.
    -    """
    -    filelist = utils.get_file_list(dir1)
    -    assert len(filelist) == 8
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_get_path_length_max(dir1) - - -

    - - -
    - -

    Test function for getting piece length for folders max.

    - -
    - Source code in tests\test_utils.py -
    71
    -72
    -73
    -74
    -75
    def test_get_path_length_max(dir1):
    -    """
    -    Test function for getting piece length for folders max.
    -    """
    -    assert utils.path_piece_length(dir1) <= (2**27)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_get_path_length_min(dir1) - - -

    - - -
    - -

    Test function for getting piece length for folders min.

    - -
    - Source code in tests\test_utils.py -
    64
    -65
    -66
    -67
    -68
    def test_get_path_length_min(dir1):
    -    """
    -    Test function for getting piece length for folders min.
    -    """
    -    assert utils.path_piece_length(dir1) >= (2**14)
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_get_path_length_mod(dir1) - - -

    - - -
    - -

    Test function for the best piece length for provided path.

    - -
    - Source code in tests\test_utils.py -
    57
    -58
    -59
    -60
    -61
    def test_get_path_length_mod(dir1):
    -    """
    -    Test function for the best piece length for provided path.
    -    """
    -    assert utils.path_piece_length(dir1) % (2**14) == 0
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_get_path_size(dir1) - - -

    - - -
    - -

    Test function for getting total size of directory.

    - -
    - Source code in tests\test_utils.py -
    110
    -111
    -112
    -113
    -114
    -115
    def test_get_path_size(dir1):
    -    """
    -    Test function for getting total size of directory.
    -    """
    -    pathsize = utils.path_size(dir1)
    -    assert pathsize == (2**18) * 8
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_get_piece_length(size) - - -

    - - -
    - -

    Test function for best piece length for given size.

    - -
    - Source code in tests\test_utils.py -
    30
    -31
    -32
    -33
    -34
    -35
    -36
    @pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945])
    -def test_get_piece_length(size):
    -    """
    -    Test function for best piece length for given size.
    -    """
    -    value = utils.get_piece_length(size)
    -    assert value % 1024 == 0
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_get_piece_length_max(size) - - -

    - - -
    - -

    Test function for best piece length for given size maximum.

    - -
    - Source code in tests\test_utils.py -
    39
    -40
    -41
    -42
    -43
    -44
    -45
    @pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945])
    -def test_get_piece_length_max(size):
    -    """
    -    Test function for best piece length for given size maximum.
    -    """
    -    value = utils.get_piece_length(size)
    -    assert value < 2**27
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_get_piece_length_min(size) - - -

    - - -
    - -

    Test function for best piece length for given size minimum.

    - -
    - Source code in tests\test_utils.py -
    48
    -49
    -50
    -51
    -52
    -53
    -54
    @pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945])
    -def test_get_piece_length_min(size):
    -    """
    -    Test function for best piece length for given size minimum.
    -    """
    -    value = utils.get_piece_length(size)
    -    assert value >= 2**14
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_humanize_bytes(amount, result) - - -

    - - -
    - -

    Test humanize bytes function.

    - -
    - Source code in tests\test_utils.py -
    160
    -161
    -162
    -163
    -164
    -165
    -166
    -167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    @pytest.mark.parametrize(
    -    "amount, result",
    -    [
    -        (100, "100"),
    -        (1100, "1 KiB"),
    -        (1_100_000, "1 MiB"),
    -        (1_100_000_000, "1 GiB"),
    -        (4_400_120_000, "4 GiB"),
    -        (4_000_120_000, "3 GiB"),
    -    ],
    -)
    -def test_humanize_bytes(amount, result):
    -    """
    -    Test humanize bytes function.
    -    """
    -    assert utils.humanize_bytes(amount) == result
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_missing_path_error() - - -

    - - -
    - -

    Test exception for missing path parameter.

    - -
    - Source code in tests\test_utils.py -
    137
    -138
    -139
    -140
    -141
    -142
    -143
    -144
    -145
    def test_missing_path_error():
    -    """
    -    Test exception for missing path parameter.
    -    """
    -    try:
    -        raise utils.MissingPathError("message")
    -    except utils.MissingPathError:
    -        assert True
    -        assert dir2
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_next_power_2(value) - - -

    - - -
    - -

    Test next power of 2 function in utils module.

    - -
    - Source code in tests\test_utils.py -
    148
    -149
    -150
    -151
    -152
    -153
    -154
    -155
    -156
    -157
    @pytest.mark.parametrize("value", [5, 32, 18, 225, 16384, 256000])
    -def test_next_power_2(value):
    -    """
    -    Test next power of 2 function in utils module.
    -    """
    -    result = utils.next_power_2(value)
    -    log = math.log2(result)
    -    assert log == int(log)
    -    assert result % 2 == 0
    -    assert result >= value
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_norm_plength_errors(amount) - - -

    - - -
    - -

    Test function to normalize piece length errors.

    - -

    Parameters:

    - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionDefault
    amount - any -

    arguments intended to raise an exception.

    - required -
    - -
    - Source code in tests\test_utils.py -
    208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    -218
    -219
    -220
    -221
    -222
    @pytest.mark.parametrize(
    -    "amount", ["hello", 11, 0, 100000, 28, "zero", "fifteen"]
    -)
    -def test_norm_plength_errors(amount):
    -    """Test function to normalize piece length errors.
    -
    -    Parameters
    -    ----------
    -    amount : any
    -        arguments intended to raise an exception.
    -    """
    -    try:
    -        assert utils.normalize_piece_length(amount)
    -    except utils.PieceLengthValueError:
    -        assert True
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_normalize_piece_length_int(amount, result) - - -

    - - -
    - -

    Test normalize piece length function.

    - -

    Parameters:

    - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionDefault
    amount -

    piece length or representation

    - required -
    result - int -

    expected output.

    - required -
    - -
    - Source code in tests\test_utils.py -
    178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    @pytest.mark.parametrize("amount, result", [(i, 2**i) for i in range(14, 25)])
    -def test_normalize_piece_length_int(amount, result):
    -    """Test normalize piece length function.
    -
    -    Parameters
    -    ----------
    -    amount : `str` or `int`
    -        piece length or representation
    -    result : int
    -        expected output.
    -    """
    -    assert utils.normalize_piece_length(amount) == result
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_normalize_piece_length_str(amount, result) - - -

    - - -
    - -

    Test normalize piece length function.

    - -

    Parameters:

    - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionDefault
    amount -

    piece length or representation

    - required -
    result - int -

    expected output.

    - required -
    - -
    - Source code in tests\test_utils.py -
    192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    @pytest.mark.parametrize(
    -    "amount, result", [(str(i), 2**i) for i in range(14, 21)]
    -)
    -def test_normalize_piece_length_str(amount, result):
    -    """Test normalize piece length function.
    -
    -    Parameters
    -    ----------
    -    amount : `str` or `int`
    -        piece length or representation
    -    result : int
    -        expected output.
    -    """
    -    assert utils.normalize_piece_length(amount) == result
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_path_stat(dir1) - - -

    - - -
    - -

    Test function for acquiring piece length information on folder.

    - -
    - Source code in tests\test_utils.py -
    78
    -79
    -80
    -81
    -82
    -83
    def test_path_stat(dir1):
    -    """
    -    Test function for acquiring piece length information on folder.
    -    """
    -    _, _, piece_length = utils.path_stat(dir1)
    -    assert piece_length % (2**14) == 0
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_path_stat_filelist_size(dir1) - - -

    - - -
    - -

    Test function for acquiring file list information on folder.

    - -
    - Source code in tests\test_utils.py -
    94
    -95
    -96
    -97
    -98
    -99
    def test_path_stat_filelist_size(dir1):
    -    """
    -    Test function for acquiring file list information on folder.
    -    """
    -    filelist, _, _ = utils.path_stat(dir1)
    -    assert len(filelist) == 8
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_path_stat_size(dir1) - - -

    - - -
    - -

    Test function for acquiring total size information on folder.

    - -
    - Source code in tests\test_utils.py -
    86
    -87
    -88
    -89
    -90
    -91
    def test_path_stat_size(dir1):
    -    """
    -    Test function for acquiring total size information on folder.
    -    """
    -    _, totalsize, _ = utils.path_stat(dir1)
    -    assert totalsize == (2**18) * 8
    -
    -
    -
    -
    - -
    - - - -
    - - - -

    -test_piecelength_error_fixtures() - - -

    - - -
    - -

    Test exception for uninterpretable piece length value.

    - -
    - Source code in tests\test_utils.py -
    126
    -127
    -128
    -129
    -130
    -131
    -132
    -133
    -134
    def test_piecelength_error_fixtures():
    -    """
    -    Test exception for uninterpretable piece length value.
    -    """
    -    try:
    -        raise utils.PieceLengthValueError("message")
    -    except utils.PieceLengthValueError:
    -        assert True
    -        assert dir1
    -
    -
    -
    -
    - -
    - - - - - -
    - -
    - -
    - - - -
    diff --git a/mkdocs.yml b/mkdocs.yml index 21669713..484cb430 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,11 +12,11 @@ theme: nav: - Home : index.md - - Help Messages : Commands.md - - Man Page: man.md + - Help : Commands.md + - Details : man.md - Changelog : changelog.md - License : Apache2.md - - Source Code : source.md + - API : source.md plugins: @@ -29,7 +29,8 @@ plugins: selection: docstring_style: numpy rendering: - show_root_heading: yes + show_root_heading: false + show_source: false docs_dir: site/ site_dir: docs/ diff --git a/pyproject.toml b/pyproject.toml index cc696056..69ffe568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] requires = ["setuptools>=40.8.0", "wheel"] +build-backend = "setuptools.build_meta" [tool.bandit] skips = ["B101"] @@ -26,6 +27,7 @@ addopts = "--maxfail=5" [tool.pylint. 'MESSAGES CONTROL'] disable = [ "R1729", + "W0108", "redefined-outer-name", "attribute-defined-outside-init", "invalid-name", diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..293d1316 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,42 @@ +[metadata] +name = torrentfile +version = attr: torrentfile.version.__version__ +author_email = attr: torrentfile.__author_email__ +description = attr: torrentfile.__description__ +keywords = Bittorrent, torrent, bittorrent-metafiles, CLI +url = attr: torrentfile.__url__ +classifiers = + Environment :: Console + Development Status :: 3 - Alpha + Operating System :: OS Independent + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Intended Audience :: End Users/Desktop + Intended Audience :: Developers + Topic :: Utilities + Topic :: Software Development :: Libraries :: Python Modules + License :: OSI Approved :: Apache Software Licens +long_description = file: README.md +long_description_content_type = text/markdown + + +[options] +zip_safe = False +include_package_data = True +packages = find: +install_requires = + pyben>="0.3.1" +setup_requires = + setuptools + wheel + +[options.entry_points] +console_scripts = + torrentfile = torrentfile.cli:main + +[options.package_data] +exclude = + .env + tests diff --git a/site/index.md b/site/index.md index 8b369b12..83b03755 100644 --- a/site/index.md +++ b/site/index.md @@ -1,6 +1,6 @@ # TorrentFile -![torrentfile](https://github.com/alexpdev/torrentfile/blob/master/assets/torrentfile.png?raw=true) +![torrentfile](https://github.com/alexpdev/torrentfile/blob/master/site/images/torrentfile.png?raw=true) ------ diff --git a/site/source.md b/site/source.md index 3ae24102..f1cf35f9 100644 --- a/site/source.md +++ b/site/source.md @@ -51,22 +51,20 @@ ------ ------ -::: torrentfile +## ::: torrentfile -::: torrentfile.cli +### ::: torrentfile.cli -::: torrentfile.edit +### ::: torrentfile.edit -::: torrentfile.hasher +### ::: torrentfile.hasher -::: torrentfile.interactive +### ::: torrentfile.interactive -::: torrentfile.recheck +### ::: torrentfile.recheck -::: torrentfile.torrent +### ::: torrentfile.torrent -::: torrentfile.utils +### ::: torrentfile.utils -::: torrentfile.version - -::: tests +### ::: torrentfile.version diff --git a/tests/test_utils.py b/tests/test_utils.py index ae3fdba6..70dbdedd 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -25,6 +25,14 @@ from tests import dir1, dir2, rmpath from torrentfile import utils +from torrentfile.__main__ import main + + +def test_main_exists(): + """ + Test if main exists + """ + assert main @pytest.mark.parametrize("size", [156634528, 2**30, 67987, 16384, 8563945]) diff --git a/torrentfile/__init__.py b/torrentfile/__init__.py index 120f4798..d638e3dc 100644 --- a/torrentfile/__init__.py +++ b/torrentfile/__init__.py @@ -39,6 +39,18 @@ from torrentfile.interactive import select_action from torrentfile.version import __version__ -__author__ = "alexpdev" - __all__ = ["execute", "create", "edit", "info", "magnet", "recheck"] + + +################################## +# torrentfile configuration data # +################################## + +__author__ = "alexpdev" +__author_email__ = "alexpdev@pm.me" +__description__ = ( + "With torrentfile you can create, edit, view, or verify the" + "contents of Bittorent v1, v2 and hybrid meta files." +) +__license__ = "Apache Software Foundation" +__url__ = "https://github.com/alexpdev" diff --git a/torrentfile/__main__.py b/torrentfile/__main__.py index 1980b3c7..28376029 100644 --- a/torrentfile/__main__.py +++ b/torrentfile/__main__.py @@ -22,13 +22,7 @@ from torrentfile.cli import execute - -def main(): - """ - Start the entry point script. - """ - execute() - +main = lambda: execute() if __name__ == "__main__": execute() # pragma: nocover diff --git a/torrentfile/cli.py b/torrentfile/cli.py index e0e68b3b..58629cbd 100644 --- a/torrentfile/cli.py +++ b/torrentfile/cli.py @@ -517,7 +517,7 @@ def execute(args=None) -> list: if hasattr(args, "func"): return args.func(args) - return args + return args # prog: nocover main_script = execute diff --git a/torrentfile/recheck.py b/torrentfile/recheck.py index 8f87c8c1..f2a4c841 100644 --- a/torrentfile/recheck.py +++ b/torrentfile/recheck.py @@ -476,11 +476,11 @@ def _gen_padding(self, partial: bytes, length: int, read=0) -> bytes: class HashChecker(ProgMixin): """ - Verify that root hashes of content files match the .torrent files. + Iterate through contents of meta data and verify with file contents. Parameters ---------- - checker : Object + checker : Checker the checker instance that maintains variables. """ diff --git a/tox.ini b/tox.ini index b69f3542..99ddce4f 100644 --- a/tox.ini +++ b/tox.ini @@ -52,4 +52,4 @@ ignore = E741, E731, E203 ignore = MC0001 [flake8] -ignore = F401 +ignore = F401, E731 From 4c62a3e94c8c0a14d400259bbe99106479f45dbe Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 May 2022 16:01:20 -0700 Subject: [PATCH 7/7] Minor styling fixes --- torrentfile/cli.py | 2 +- torrentfile/commands.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/torrentfile/cli.py b/torrentfile/cli.py index 58629cbd..717800bd 100644 --- a/torrentfile/cli.py +++ b/torrentfile/cli.py @@ -517,7 +517,7 @@ def execute(args=None) -> list: if hasattr(args, "func"): return args.func(args) - return args # prog: nocover + return args # pragma: nocover main_script = execute diff --git a/torrentfile/commands.py b/torrentfile/commands.py index a999301f..2319e1fa 100644 --- a/torrentfile/commands.py +++ b/torrentfile/commands.py @@ -92,9 +92,9 @@ def info(args: list): """ metafile = args.metafile meta = pyben.load(metafile) - info = meta["info"] + data = meta["info"] del meta["info"] - meta.update(info) + meta.update(data) if "private" in meta and meta["private"] == 1: meta["private"] = "True" if "announce-list" in meta: @@ -192,14 +192,14 @@ def magnet(metafile: str): raise FileNotFoundError meta = pyben.load(metafile) - info = meta["info"] - binfo = pyben.dumps(info) + data = meta["info"] + binfo = pyben.dumps(data) infohash = sha1(binfo).hexdigest().upper() # nosec logger.info("Magnet Info Hash: %s", infohash) scheme = "magnet:" hasharg = "?xt=urn:btih:" + infohash - namearg = "&dn=" + quote_plus(info["name"]) + namearg = "&dn=" + quote_plus(data["name"]) if "announce-list" in meta: announce_args = [