API - TorrentFile

# TorrentFile API Documentation

## `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.

Classes

- `HelpFormat` — Formatting class for help tips provided by the CLI.

Functions

- `create_magnet``(``metafile``)` (`str`) — Create a magnet URI from a Bittorrent meta file.
- `main``(``)` — Initiate main function for CLI script.
- `main_script``(``args``)` — Initialize Command Line Interface for torrentfile.

Something Clever

## `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.

### `HelpFormat (HelpFormatter)`

Formatting class for help tips provided by the CLI.

Subclasses Argparse.HelpFormatter.

Source code in `torrentfile\cli.py`

    class HelpFormat(HelpFormatter):
        """
        Formatting class for help tips provided by the CLI.

        Subclasses Argparse.HelpFormatter.
        """

        def __init__(self, prog, width=75, max_help_positions=60):
            """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`
                string that needs formatting.
            """
            text = text % dict(prog=self._prog) if "%(prog)" in text else text
            text = self._whitespace_matcher.sub(" ", text).strip()
            return text + "\n\n"

#### `__init__(self, prog, width=75, max_help_positions=60)` `special`

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. | `75`
`max_help_positions` | ``int`` | max length until line wrap. | `60`

Source code in `torrentfile\cli.py`

    def __init__(self, prog, width=75, max_help_positions=60):
        """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
        )

### `create_magnet(metafile)`

Create a magnet URI from a Bittorrent meta file.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`metafile` | ``str` | `os.PathLike`` | path to bittorrent meta file. | required

Returns:

Type | Description
--- | ---
``str`` | created magnet URI.

Source code in `torrentfile\cli.py`

    def create_magnet(metafile):
        """Create a magnet URI from a Bittorrent meta file.

        Parameters
        ----------
        metafile : `str` | `os.PathLike`
            path to bittorrent meta file.

        Returns
        -------
        `str`
            created magnet URI.
        """
        import os
        from hashlib import sha1  # nosec
        from urllib.parse import quote_plus

        import pyben

        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
        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)
        sys.stdout.write(full_uri)
        return full_uri

### `main()`

Initiate main function for CLI script.

Source code in `torrentfile\cli.py`

    def main():
        """Initiate main function for CLI script."""
        main_script()

### `main_script(args=None)`

Initialize Command Line Interface for torrentfile.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`args` | ``list`` | Commandline arguments. default=None | `None`

Source code in `torrentfile\cli.py`

    def main_script(args=None):
        """Initialize Command Line Interface for torrentfile.

        Parameters
        ----------
        args : `list`
            Commandline arguments. default=None
        """
        if not args:
            if sys.argv[1:]:
                args = sys.argv[1:]
            else:
                args = ["-h"]

        parser = ArgumentParser(
            "TorrentFile",
            description="""
            CLI Tool for creating, checking and editing Bittorrent meta files.
            Supports all meta file versions including hybrid files.
            """,
            prefix_chars="-",
            formatter_class=HelpFormat,
        )

        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{torrentfile.__version__}",
            help="show program version and exit",
        )

        parser.add_argument(
            "-v",
            "--verbose",
            action="store_true",
            dest="debug",
            help="output debug information",
        )

        subparsers = parser.add_subparsers(
            title="Actions",
            description="Each sub-command triggers a specific action.",
            dest="command",
        )

        create_parser = subparsers.add_parser(
            "c",
            help="""
            Create a torrent meta file.
            """,
            prefix_chars="-",
            aliases=["create", "new"],
            formatter_class=HelpFormat,
        )

        create_parser.add_argument(
            "-a",
            "--announce",
            action="store",
            dest="announce",
            metavar="<url>",
            nargs="+",
            default=[],
            help="Alias for -t/--tracker",
        )

        create_parser.add_argument(
            "-p",
            "--private",
            action="store_true",
            dest="private",
            help="Create a private torrent meta file",
        )

        create_parser.add_argument(
            "-s",
            "--source",
            action="store",
            dest="source",
            metavar="<source>",
            help="specify source tracker",
        )

        create_parser.add_argument(
            "-m",
            "--magnet",
            action="store_true",
            dest="magnet",
            help="output Magnet Link after creation completes",
        )

        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 path for created .torrent file",
        )

        create_parser.add_argument(
            "-t",
            "--tracker",
            action="store",
            dest="tracker",
            metavar="<url>",
            nargs="+",
            default=[],
            help="""One or more Bittorrent tracker announce url(s).""",
        )

        create_parser.add_argument(
            "--progress",
            action="store_true",
            dest="progress",
            help="""
            Enable showing the progress bar during torrent creation.
            (Minimially impacts the duration of torrent file creation.)
            """,
        )

        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 or 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="""
            Fixed amount of bytes for each chunk of data. (Default: None)
            Acceptable input values include integers 14-24, which
            will be interpreted as the exponent for 2^n, or any perfect
            power of two integer between 16Kib and 16MiB (inclusive).
            Examples:: [--piece-length 14] [-l 20] [-l 16777216]
            """,
        )

        create_parser.add_argument(
            "-w",
            "--web-seed",
            action="store",
            dest="url_list",
            metavar="<url>",
            nargs="+",
            help="""
            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]]]
            """,
        )

        create_parser.add_argument(
            "content",
            action="store",
            metavar="<content path>",
            help="path to content file or directory",
        )

        edit_parser = subparsers.add_parser(
            "e",
            help="""
            Edit existing torrent meta file.
            """,
            aliases=["edit"],
            prefix_chars="-",
            formatter_class=HelpFormat,
        )

        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 space seperated url(s)
            """,
        )

        edit_parser.add_argument(
            "--private",
            action="store_true",
            help="If currently private, will make it public, if public then 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>",
        )

        magnet_parser = subparsers.add_parser(
            "m",
            help="""
            Create magnet url from an existing Bittorrent meta file.
            """,
            aliases=["magnet"],
            prefix_chars="-",
            formatter_class=HelpFormat,
        )

        magnet_parser.add_argument(
            "metafile",
            action="store",
            help="path to Bittorrent meta file.",
            metavar="<*.torrent>",
        )

        check_parser = subparsers.add_parser(
            "r",
            help="""
            Calculate amount of torrent meta file's content is found on disk.
            """,
            aliases=["recheck", "check"],
            prefix_chars="-",
            formatter_class=HelpFormat,
        )

        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",
        )

        flags = parser.parse_args(args)

        if flags.debug:
            torrentfile.set_level(logging.DEBUG)

        logger.debug(str(flags))
        if flags.interactive:
            return select_action()

        if flags.command in ["m", "magnet"]:
            return create_magnet(flags.metafile)

        if flags.command in ["recheck", "r", "check"]:
            logger.debug("Program entering Recheck mode.")
            metafile = flags.metafile
            content = flags.content
            logger.debug("Checking %s against %s contents", metafile, content)
            checker = Checker(metafile, content)
            logger.debug("Completed initialization of the Checker class")
            result = checker.results()
            logger.info("Final result for %s recheck:  %s", metafile, result)
            sys.stdout.write(str(result))
            sys.stdout.flush()
            return result

        if flags.command in ["edit", "e"]:
            metafile = flags.metafile
            logger.info("Editing %s Meta File", str(flags.metafile))
            editargs = {
                "url-list": flags.url_list,
                "announce": flags.announce,
                "source": flags.source,
                "private": flags.private,
                "comment": flags.comment,
            }
            return edit_torrent(metafile, editargs)

        kwargs = {
            "progress": flags.progress,
            "url_list": flags.url_list,
            "path": flags.content,
            "announce": flags.announce + flags.tracker,
            "piece_length": flags.piece_length,
            "source": flags.source,
            "private": flags.private,
            "outfile": flags.outfile,
            "comment": flags.comment,
        }

        logger.debug("Program has entered torrent creation mode.")

        if flags.meta_version == "2":
            torrent = TorrentFileV2(**kwargs)
        elif flags.meta_version == "3":
            torrent = TorrentFileHybrid(**kwargs)
        else:
            torrent = TorrentFile(**kwargs)
        logger.debug("Completed torrent files meta info assembly.")
        outfile, meta = torrent.write()
        if flags.magnet:
            create_magnet(outfile)
        parser.kwargs = kwargs
        parser.meta = meta
        parser.outfile = outfile
        logger.debug("New torrent file (%s) has been created.", str(outfile))
        return parser

## `torrentfile.cli.HelpFormat (HelpFormatter)`

Formatting class for help tips provided by the CLI.

Subclasses Argparse.HelpFormatter.

Source code in `torrentfile\cli.py`

    class HelpFormat(HelpFormatter):
        """
        Formatting class for help tips provided by the CLI.

        Subclasses Argparse.HelpFormatter.
        """

        def __init__(self, prog, width=75, max_help_positions=60):
            """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`
                string that needs formatting.
            """
            text = text % dict(prog=self._prog) if "%(prog)" in text else text
            text = self._whitespace_matcher.sub(" ", text).strip()
            return text + "\n\n"

### `__init__(self, prog, width=75, max_help_positions=60)` `special`

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. | `75`
`max_help_positions` | ``int`` | max length until line wrap. | `60`

Source code in `torrentfile\cli.py`

    def __init__(self, prog, width=75, max_help_positions=60):
        """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
        )

## `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.

Attention

All strings in a .torrent file that contains 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

### Version 1 meta-dictionary

-announce: The URL of the tracker.

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

## Version 1 info-dictionary

- `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.

Important

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` — Base Class for all TorrentFile classes.
- `TorrentFile` — Class for creating Bittorrent meta files.
- `TorrentFileV2` — Class for creating Bittorrent meta v2 files.
- `TorrentFileHybrid` — Construct the Hybrid torrent meta file with provided parameters.

## `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.

Attention

All strings in a .torrent file that contains 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

##### Version 1 meta-dictionary

-announce: The URL of the tracker.

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

#### Version 1 info-dictionary

- `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.

Important

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` | ``bool`` | If True disable showing the progress bar. | `False`

Source code in `torrentfile\torrent.py`

    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 : `bool`
            If True disable showing the progress bar.
        """

        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)

        # fmt: off
        def __init__(self, path=None, announce=None, private=False,
                     source=None, piece_length=None, comment=None,
                     outfile=None, url_list=None, progress=False):
            """Construct MetaFile superclass and assign local attributes."""
            if not path:
                raise utils.MissingPathError

            # base path to torrent content.
            self.path = path

            # Format piece_length attribute.
            if piece_length:
                self.piece_length = utils.normalize_piece_length(piece_length)
            else:
                self.piece_length = utils.path_piece_length(self.path)

            # 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 = announce
                self.announce_list = [announce]
            elif isinstance(announce, Sequence):
                self.announce = announce[0]
                self.announce_list = [announce]

            if private:
                self.private = 1
            else:
                self.private = None

            self.outfile = outfile
            self.progress = progress
            self.comment = comment
            self.url_list = url_list
            self.source = source
            self.meta = {
                "announce": self.announce,
                "announce-list": self.announce_list,
                "created by": f"TorrentFile:v{version}",
                "creation date": int(datetime.timestamp(datetime.now())),
                "info": {},
            }
            logger.debug("Announce list = %s", str(self.announce_list))
            if comment:
                self.meta["info"]["comment"] = comment
            if private:
                self.meta["info"]["private"] = 1
            if source:
                self.meta["info"]["source"] = source
            if url_list:
                self.meta["url-list"] = url_list
            self.meta["info"]["name"] = os.path.basename(self.path)
            self.meta["info"]["piece length"] = self.piece_length
        # fmt: on

        def assemble(self):
            """Overload in subclasses.

            Raises
            ------
            `Exception`
                NotImplementedError
            """
            raise NotImplementedError

        def sort_meta(self):
            """Sort the info and meta dictionaries."""
            meta = self.meta
            meta["info"] = dict(sorted(list(meta["info"].items())))
            meta = dict(sorted(list(meta.items())))
            return meta

        def write(self, outfile=None):
            """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.
            """
            if outfile is not None:
                self.outfile = outfile

            if self.outfile is None:
                self.outfile = str(self.path) + ".torrent"

            self.meta = self.sort_meta()
            pyben.dump(self.meta, self.outfile)
            return self.outfile, self.meta

#### `__init__(self, path=None, announce=None, private=False, source=None, piece_length=None, comment=None, outfile=None, url_list=None, progress=False)` `special`

Construct MetaFile superclass and assign local attributes.

Source code in `torrentfile\torrent.py`

    def __init__(self, path=None, announce=None, private=False,
                 source=None, piece_length=None, comment=None,
                 outfile=None, url_list=None, progress=False):
        """Construct MetaFile superclass and assign local attributes."""
        if not path:
            raise utils.MissingPathError

        # base path to torrent content.
        self.path = path

        # Format piece_length attribute.
        if piece_length:
            self.piece_length = utils.normalize_piece_length(piece_length)
        else:
            self.piece_length = utils.path_piece_length(self.path)

        # 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 = announce
            self.announce_list = [announce]
        elif isinstance(announce, Sequence):
            self.announce = announce[0]
            self.announce_list = [announce]

        if private:
            self.private = 1
        else:
            self.private = None

        self.outfile = outfile
        self.progress = progress
        self.comment = comment
        self.url_list = url_list
        self.source = source
        self.meta = {
            "announce": self.announce,
            "announce-list": self.announce_list,
            "created by": f"TorrentFile:v{version}",
            "creation date": int(datetime.timestamp(datetime.now())),
            "info": {},
        }
        logger.debug("Announce list = %s", str(self.announce_list))
        if comment:
            self.meta["info"]["comment"] = comment
        if private:
            self.meta["info"]["private"] = 1
        if source:
            self.meta["info"]["source"] = source
        if url_list:
            self.meta["url-list"] = url_list
        self.meta["info"]["name"] = os.path.basename(self.path)
        self.meta["info"]["piece length"] = self.piece_length

#### `assemble(self)`

Overload in subclasses.

Exceptions:

Type | Description
--- | ---
``Exception`` | NotImplementedError

Source code in `torrentfile\torrent.py`

    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`

    @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(self)`

Sort the info and meta dictionaries.

Source code in `torrentfile\torrent.py`

    def sort_meta(self):
        """Sort the info and meta dictionaries."""
        meta = self.meta
        meta["info"] = dict(sorted(list(meta["info"].items())))
        meta = dict(sorted(list(meta.items())))
        return meta

#### `write(self, outfile=None)`

Write meta information to .torrent file.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`outfile` | ``str`` | Destination path for .torrent file. default=None | `None`

Returns:

Type | Description
--- | ---
``str`` | Where the .torrent file was writen.

Source code in `torrentfile\torrent.py`

    def write(self, outfile=None):
        """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.
        """
        if outfile is not None:
            self.outfile = outfile

        if self.outfile is None:
            self.outfile = str(self.path) + ".torrent"

        self.meta = self.sort_meta()
        pyben.dump(self.meta, self.outfile)
        return self.outfile, self.meta

### `TorrentFile (MetaFile)`

Class for creating Bittorrent meta files.

Construct Torrentfile class instance object.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`path` | ``str`` | Path to torrent file or directory. | required
`piece_length` | ``int`` | Size of each piece of torrent data. | required
`announce` | ``str` or `list`` | One or more tracker URL's. | required
`private` | ``int`` | 1 if private torrent else 0. | required
`source` | ``str`` | Source tracker. | required
`comment` | ``str`` | Comment string. | required
`outfile` | ``str`` | Path to write metfile to. | required

Source code in `torrentfile\torrent.py`

    class TorrentFile(MetaFile):
        """Class for creating Bittorrent meta files.

        Construct *Torrentfile* class instance object.

        Parameters
        ----------
        path : `str`
            Path to torrent file or directory.
        piece_length : `int`
            Size of each piece of torrent data.
        announce : `str` or `list`
            One or more tracker URL's.
        private : `int`
            1 if private torrent else 0.
        source : `str`
            Source tracker.
        comment : `str`
            Comment string.
        outfile : `str`
            Path to write metfile to.
        """

        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("Making Bittorrent V1 meta 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)
            if self.progress:
                from tqdm import tqdm

                for piece in tqdm(
                    iterable=feeder,
                    desc="Hashing Content",
                    total=size // self.piece_length,
                    unit="bytes",
                    unit_scale=True,
                    unit_divisor=self.piece_length,
                    initial=0,
                    leave=True,
                ):
                    pieces.extend(piece)
            else:
                for piece in feeder:
                    pieces.extend(piece)
            info["pieces"] = pieces

#### `hasher (CbMixin)`

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
`total` | ``int`` | Sum of all files in file list. | required

Source code in `torrentfile\torrent.py`

    class Hasher(CbMixin):
        """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.
        total : `int`
            Sum of all files in file list.
        """

        def __init__(self, paths, piece_length):
            """Generate hashes of piece length data from filelist contents."""
            self.piece_length = piece_length
            self.paths = paths
            self.total = sum([os.path.getsize(i) for i in self.paths])
            self.index = 0
            self.current = open(self.paths[0], "rb")
            logger.debug(
                "Hashing v1 torrent file. Size: %s Piece Length: %s",
                humanize_bytes(self.total),
                humanize_bytes(self.piece_length),
            )

        def __iter__(self):
            """Iterate through feed pieces.

            Returns
            -------
            self : `iterator`
                Iterator for leaves/hash pieces.
            """
            return self

        def _handle_partial(self, arr):
            """Define the handling partial pieces that span 2 or more files.

            Parameters
            ----------
            arr : `bytearray`
                Incomplete piece containing partial data
            partial : `int`
                Size of incomplete piece_length

            Returns
            -------
            digest : `bytes`
                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])
                if size == target:
                    break
            return sha1(arr).digest()  # nosec

        def next_file(self):
            """Seemlessly transition to next file in file list."""
            self.index += 1
            if self.index < len(self.paths):
                self.current.close()
                self.current = open(self.paths[self.index], "rb")
                return True
            return False

        def __next__(self):
            """Generate piece-length pieces of data from input file list."""
            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:
                    return self._handle_partial(piece[:size])
                else:
                    return sha1(piece).digest()  # nosec

##### `__init__(self, paths, piece_length)` `special`

Generate hashes of piece length data from filelist contents.

Source code in `torrentfile\torrent.py`

    def __init__(self, paths, piece_length):
        """Generate hashes of piece length data from filelist contents."""
        self.piece_length = piece_length
        self.paths = paths
        self.total = sum([os.path.getsize(i) for i in self.paths])
        self.index = 0
        self.current = open(self.paths[0], "rb")
        logger.debug(
            "Hashing v1 torrent file. Size: %s Piece Length: %s",
            humanize_bytes(self.total),
            humanize_bytes(self.piece_length),
        )

##### `__iter__(self)` `special`

Iterate through feed pieces.

Returns:

Type | Description
--- | ---
``iterator`` | Iterator for leaves/hash pieces.

Source code in `torrentfile\torrent.py`

    def __iter__(self):
        """Iterate through feed pieces.

        Returns
        -------
        self : `iterator`
            Iterator for leaves/hash pieces.
        """
        return self

##### `__next__(self)` `special`

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

Source code in `torrentfile\torrent.py`

    def __next__(self):
        """Generate piece-length pieces of data from input file list."""
        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:
                return self._handle_partial(piece[:size])
            else:
                return sha1(piece).digest()  # nosec

##### `next_file(self)`

Seemlessly transition to next file in file list.

Source code in `torrentfile\torrent.py`

    def next_file(self):
        """Seemlessly transition to next file in file list."""
        self.index += 1
        if self.index < len(self.paths):
            self.current.close()
            self.current = open(self.paths[self.index], "rb")
            return True
        return False

#### `__init__(self, **kwargs)` `special`

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`

    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("Making Bittorrent V1 meta file.")
        self.assemble()

#### `assemble(self)`

Assemble components of torrent metafile.

Returns:

Type | Description
--- | ---
``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)
        if self.progress:
            from tqdm import tqdm

            for piece in tqdm(
                iterable=feeder,
                desc="Hashing Content",
                total=size // self.piece_length,
                unit="bytes",
                unit_scale=True,
                unit_divisor=self.piece_length,
                initial=0,
                leave=True,
            ):
                pieces.extend(piece)
        else:
            for piece in feeder:
                pieces.extend(piece)
        info["pieces"] = pieces

### `TorrentFileHybrid (MetaFile)`

Construct the Hybrid torrent meta file with provided parameters.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`path` | ``str`` | path to torrentfile target. | required
`announce` | ``str` or `list`` | one or more tracker URL's. | required
`comment` | ``str`` | Some comment. | required
`source` | ``str`` | Used for private trackers. | required
`outfile` | ``str`` | target path to write output. | required
`private` | ``bool`` | Used for private trackers. | required
`piece_length` | ``int`` | torrentfile data piece length. | required

Source code in `torrentfile\torrent.py`

    class TorrentFileHybrid(MetaFile):
        """Construct the Hybrid torrent meta file with provided parameters.

        Parameters
        ----------
        path : `str`
            path to torrentfile target.
        announce : `str` or `list`
            one or more tracker URL's.
        comment : `str`
            Some comment.
        source : `str`
            Used for private trackers.
        outfile : `str`
            target path to write output.
        private : `bool`
            Used for private trackers.
        piece_length : `int`
            torrentfile data piece length.
        """

        hasher = HasherHybrid

        def __init__(self, **kwargs):
            """Create Bittorrent v1 v2 hybrid metafiles."""
            super().__init__(**kwargs)
            logger.debug("Creating Hybrid torrent file.")
            self.name = os.path.basename(self.path)
            self.hashes = []
            self.piece_layers = {}
            self.pbar = None
            self.pieces = []
            self.files = []
            self.assemble()

        def assemble(self):
            """Assemble the parts of the torrentfile into meta dictionary."""
            info = self.meta["info"]
            info["meta version"] = 2
            if self.progress:
                from tqdm import tqdm

                lst = utils.get_file_list(self.path)
                self.pbar = tqdm(
                    desc="Hashing Files:",
                    total=len(lst),
                    leave=True,
                    unit="file",
                )

            if os.path.isfile(self.path):
                info["file tree"] = {self.name: self._traverse(self.path)}
                info["length"] = os.path.getsize(self.path)
                if self.pbar:
                    self.pbar.update(n=1)
            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):
            """Build meta dictionary while walking directory.

            Parameters
            ----------
            path : `str`
                Path to target file.
            """
            if os.path.isfile(path):
                fsize = os.path.getsize(path)

                self.files.append(
                    {
                        "length": fsize,
                        "path": os.path.relpath(path, self.path).split(os.sep),
                    }
                )

                if fsize == 0:
                    if self.pbar:
                        self.pbar.update(n=1)
                    return {"": {"length": fsize}}

                fhash = HasherHybrid(path, self.piece_length)

                if fsize > self.piece_length:
                    self.piece_layers[fhash.root] = fhash.piece_layer

                self.hashes.append(fhash)
                self.pieces.extend(fhash.pieces)

                if fhash.padding_file:
                    self.files.append(fhash.padding_file)

                if self.pbar:
                    self.pbar.update(n=1)

                return {"": {"length": fsize, "pieces root": fhash.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

#### `hasher (CbMixin)`

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

Source code in `torrentfile\torrent.py`

    class HasherHybrid(CbMixin):
        """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.
        """

        def __init__(self, path, piece_length):
            """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
            logger.debug(
                "Hashing partial Hybrid torrent file. Piece Length: %s Path: %s",
                humanize_bytes(self.piece_length),
                str(self.path),
            )
            with open(path, "rb") as data:
                self._process_file(data)

        def _pad_remaining(self, block_count):
            """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 _process_file(self, data):
            """Calculate layer hashes for contents of file.

            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)
                    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": size,
                        "path": [".pad", str(plength)],
                    }
                    piece.update(bytes(plength))
                self.pieces.append(piece.digest())  # nosec
            self._calculate_root()

        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)

##### `__init__(self, path, piece_length)` `special`

Construct Hasher class instances for each file in torrent.

Source code in `torrentfile\torrent.py`

    def __init__(self, path, piece_length):
        """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
        logger.debug(
            "Hashing partial Hybrid torrent file. Piece Length: %s Path: %s",
            humanize_bytes(self.piece_length),
            str(self.path),
        )
        with open(path, "rb") as data:
            self._process_file(data)

#### `__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("Creating Hybrid torrent file.")
        self.name = os.path.basename(self.path)
        self.hashes = []
        self.piece_layers = {}
        self.pbar = None
        self.pieces = []
        self.files = []
        self.assemble()

#### `assemble(self)`

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 self.progress:
            from tqdm import tqdm

            lst = utils.get_file_list(self.path)
            self.pbar = tqdm(
                desc="Hashing Files:",
                total=len(lst),
                leave=True,
                unit="file",
            )

        if os.path.isfile(self.path):
            info["file tree"] = {self.name: self._traverse(self.path)}
            info["length"] = os.path.getsize(self.path)
            if self.pbar:
                self.pbar.update(n=1)
        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 (MetaFile)`

Class for creating Bittorrent meta v2 files.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`path` | ``str`` | Path to torrent file or directory. | required
`piece_length` | ``int`` | Size of each piece of torrent data. | required
`announce` | ``str` or `list`` | one or more tracker URL's. | required
`private` | ``int`` | 1 if private torrent else 0. | required
`source` | ``str`` | Source tracker. | required
`comment` | ``str`` | Comment string. | required
`outfile` | ``str`` | Path to write metfile to. | required

Source code in `torrentfile\torrent.py`

    class TorrentFileV2(MetaFile):
        """Class for creating Bittorrent meta v2 files.

        Parameters
        ----------
        path : `str`
            Path to torrent file or directory.
        piece_length : `int`
            Size of each piece of torrent data.
        announce : `str` or `list`
            one or more tracker URL's.
        private : `int`
            1 if private torrent else 0.
        source : `str`
            Source tracker.
        comment : `str`
            Comment string.
        outfile : `str`
            Path to write metfile to.
        """

        hasher = HasherV2

        def __init__(self, **kwargs):
            """Construct `TorrentFileV2` Class instance from given parameters.

            Parameters
            ----------
            kwargs : `dict`
                keywword arguments to pass to superclass.
            """
            super().__init__(**kwargs)
            logger.debug("Create .torrent v2 file.")
            self.piece_layers = {}
            self.hashes = []
            self.pbar = None
            self.assemble()

        def update(self):
            """Update for the progress bar."""
            if self.pbar:
                self.pbar.update(n=1)

        def assemble(self):
            """Assemble then return the meta dictionary for encoding.

            Returns
            -------
            meta : `dict`
                Metainformation about the torrent.
            """
            info = self.meta["info"]

            if self.progress:
                from tqdm import tqdm

                lst = utils.get_file_list(self.path)
                self.pbar = tqdm(
                    desc="Hashing Files:",
                    total=len(lst),
                    leave=True,
                    unit="file",
                )

            if os.path.isfile(self.path):
                info["file tree"] = {info["name"]: self._traverse(self.path)}
                info["length"] = os.path.getsize(self.path)
                self.update()
            else:
                info["file tree"] = self._traverse(self.path)

            info["meta version"] = 2
            self.meta["piece layers"] = self.piece_layers

        def _traverse(self, path):
            """Walk directory tree.

            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:
                    self.update()
                    return {"": {"length": size}}

                fhash = HasherV2(path, self.piece_length)

                if size > self.piece_length:
                    self.piece_layers[fhash.root] = fhash.piece_layer
                self.update()
                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)`

Calculate the root hash and piece layers for file contents.

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

Source code in `torrentfile\torrent.py`

    class HasherV2(CbMixin):
        """Calculate the root hash and piece layers for file contents.

        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.
        """

        def __init__(self, path, piece_length):
            """Calculate and store hash information for specific file."""
            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
            logger.debug(
                "Hashing partial v2 torrent file. Piece Length: %s Path: %s",
                humanize_bytes(self.piece_length),
                str(self.path),
            )

            with open(self.path, "rb") as fd:
                self.process_file(fd)

        def process_file(self, fd):
            """Calculate hashes over 16KiB chuncks of file content.

            Parameters
            ----------
            fd : `str`
                Opened file in read mode.
            """
            while True:
                total = 0
                blocks = []
                leaf = bytearray(BLOCK_SIZE)
                # generate leaves of merkle tree

                for _ in range(self.num_blocks):
                    size = fd.readinto(leaf)
                    total += 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)]
                    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()

        def _calculate_root(self):
            """Calculate root hash for the target file."""
            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__(self, path, piece_length)` `special`

Calculate and store hash information for specific file.

Source code in `torrentfile\torrent.py`

    def __init__(self, path, piece_length):
        """Calculate and store hash information for specific file."""
        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
        logger.debug(
            "Hashing partial v2 torrent file. Piece Length: %s Path: %s",
            humanize_bytes(self.piece_length),
            str(self.path),
        )

        with open(self.path, "rb") as fd:
            self.process_file(fd)

##### `process_file(self, fd)`

Calculate hashes over 16KiB chuncks of file content.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`fd` | ``str`` | Opened file in read mode. | required

Source code in `torrentfile\torrent.py`

    def process_file(self, fd):
        """Calculate hashes over 16KiB chuncks of file content.

        Parameters
        ----------
        fd : `str`
            Opened file in read mode.
        """
        while True:
            total = 0
            blocks = []
            leaf = bytearray(BLOCK_SIZE)
            # generate leaves of merkle tree

            for _ in range(self.num_blocks):
                size = fd.readinto(leaf)
                total += 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)]
                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()

#### `__init__(self, **kwargs)` `special`

Construct `TorrentFileV2` Class instance from given parameters.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`kwargs` | ``dict`` | keywword arguments to pass to superclass. | `{}`

Source code in `torrentfile\torrent.py`

    def __init__(self, **kwargs):
        """Construct `TorrentFileV2` Class instance from given parameters.

        Parameters
        ----------
        kwargs : `dict`
            keywword arguments to pass to superclass.
        """
        super().__init__(**kwargs)
        logger.debug("Create .torrent v2 file.")
        self.piece_layers = {}
        self.hashes = []
        self.pbar = None
        self.assemble()

#### `assemble(self)`

Assemble then return the meta dictionary for encoding.

Returns:

Type | Description
--- | ---
``dict`` | Metainformation about the torrent.

Source code in `torrentfile\torrent.py`

    def assemble(self):
        """Assemble then return the meta dictionary for encoding.

        Returns
        -------
        meta : `dict`
            Metainformation about the torrent.
        """
        info = self.meta["info"]

        if self.progress:
            from tqdm import tqdm

            lst = utils.get_file_list(self.path)
            self.pbar = tqdm(
                desc="Hashing Files:",
                total=len(lst),
                leave=True,
                unit="file",
            )

        if os.path.isfile(self.path):
            info["file tree"] = {info["name"]: self._traverse(self.path)}
            info["length"] = os.path.getsize(self.path)
            self.update()
        else:
            info["file tree"] = self._traverse(self.path)

        info["meta version"] = 2
        self.meta["piece layers"] = self.piece_layers

#### `update(self)`

Update for the progress bar.

Source code in `torrentfile\torrent.py`

    def update(self):
        """Update for the progress bar."""
        if self.pbar:
            self.pbar.update(n=1)

## `Hasher` Module

module
`torrentfile.` `hasher`

Piece/File Hashers for Bittorrent meta file contents.

Classes

- `CbMixin` — Mixin class to set a callback during hashing procedure.
- `Hasher` — Piece hasher for Bittorrent V1 files.
- `HasherV2` — Calculate the root hash and piece layers for file contents.
- `HasherHybrid` — Calculate root and piece hashes for creating hybrid torrent file.

Functions

- `merkle_root``(``blocks``)` — Calculate the merkle root for a seq of sha256 hash digests.

## `torrentfile.hasher.merkle_root(blocks)`

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

Source code in `torrentfile\hasher.py`

    def merkle_root(blocks):
        """Calculate the merkle root for a seq of sha256 hash digests."""
        while len(blocks) > 1:
            blocks = [sha256(x + y).digest() for x, y in zip(*[iter(blocks)] * 2)]
        return blocks[0]

## `torrentfile.hasher.Hasher (CbMixin)`

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
`total` | ``int`` | Sum of all files in file list. | required

Source code in `torrentfile\hasher.py`

    class Hasher(CbMixin):
        """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.
        total : `int`
            Sum of all files in file list.
        """

        def __init__(self, paths, piece_length):
            """Generate hashes of piece length data from filelist contents."""
            self.piece_length = piece_length
            self.paths = paths
            self.total = sum([os.path.getsize(i) for i in self.paths])
            self.index = 0
            self.current = open(self.paths[0], "rb")
            logger.debug(
                "Hashing v1 torrent file. Size: %s Piece Length: %s",
                humanize_bytes(self.total),
                humanize_bytes(self.piece_length),
            )

        def __iter__(self):
            """Iterate through feed pieces.

            Returns
            -------
            self : `iterator`
                Iterator for leaves/hash pieces.
            """
            return self

        def _handle_partial(self, arr):
            """Define the handling partial pieces that span 2 or more files.

            Parameters
            ----------
            arr : `bytearray`
                Incomplete piece containing partial data
            partial : `int`
                Size of incomplete piece_length

            Returns
            -------
            digest : `bytes`
                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])
                if size == target:
                    break
            return sha1(arr).digest()  # nosec

        def next_file(self):
            """Seemlessly transition to next file in file list."""
            self.index += 1
            if self.index < len(self.paths):
                self.current.close()
                self.current = open(self.paths[self.index], "rb")
                return True
            return False

        def __next__(self):
            """Generate piece-length pieces of data from input file list."""
            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:
                    return self._handle_partial(piece[:size])
                else:
                    return sha1(piece).digest()  # nosec

### `__init__(self, paths, piece_length)` `special`

Generate hashes of piece length data from filelist contents.

Source code in `torrentfile\hasher.py`

    def __init__(self, paths, piece_length):
        """Generate hashes of piece length data from filelist contents."""
        self.piece_length = piece_length
        self.paths = paths
        self.total = sum([os.path.getsize(i) for i in self.paths])
        self.index = 0
        self.current = open(self.paths[0], "rb")
        logger.debug(
            "Hashing v1 torrent file. Size: %s Piece Length: %s",
            humanize_bytes(self.total),
            humanize_bytes(self.piece_length),
        )

### `__iter__(self)` `special`

Iterate through feed pieces.

Returns:

Type | Description
--- | ---
``iterator`` | Iterator for leaves/hash pieces.

Source code in `torrentfile\hasher.py`

    def __iter__(self):
        """Iterate through feed pieces.

        Returns
        -------
        self : `iterator`
            Iterator for leaves/hash pieces.
        """
        return self

### `__next__(self)` `special`

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

Source code in `torrentfile\hasher.py`

    def __next__(self):
        """Generate piece-length pieces of data from input file list."""
        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:
                return self._handle_partial(piece[:size])
            else:
                return sha1(piece).digest()  # nosec

### `next_file(self)`

Seemlessly transition to next file in file list.

Source code in `torrentfile\hasher.py`

    def next_file(self):
        """Seemlessly transition to next file in file list."""
        self.index += 1
        if self.index < len(self.paths):
            self.current.close()
            self.current = open(self.paths[self.index], "rb")
            return True
        return False

## `torrentfile.hasher.HasherV2 (CbMixin)`

Calculate the root hash and piece layers for file contents.

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

Source code in `torrentfile\hasher.py`

    class HasherV2(CbMixin):
        """Calculate the root hash and piece layers for file contents.

        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.
        """

        def __init__(self, path, piece_length):
            """Calculate and store hash information for specific file."""
            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
            logger.debug(
                "Hashing partial v2 torrent file. Piece Length: %s Path: %s",
                humanize_bytes(self.piece_length),
                str(self.path),
            )

            with open(self.path, "rb") as fd:
                self.process_file(fd)

        def process_file(self, fd):
            """Calculate hashes over 16KiB chuncks of file content.

            Parameters
            ----------
            fd : `str`
                Opened file in read mode.
            """
            while True:
                total = 0
                blocks = []
                leaf = bytearray(BLOCK_SIZE)
                # generate leaves of merkle tree

                for _ in range(self.num_blocks):
                    size = fd.readinto(leaf)
                    total += 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)]
                    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()

        def _calculate_root(self):
            """Calculate root hash for the target file."""
            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__(self, path, piece_length)` `special`

Calculate and store hash information for specific file.

Source code in `torrentfile\hasher.py`

    def __init__(self, path, piece_length):
        """Calculate and store hash information for specific file."""
        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
        logger.debug(
            "Hashing partial v2 torrent file. Piece Length: %s Path: %s",
            humanize_bytes(self.piece_length),
            str(self.path),
        )

        with open(self.path, "rb") as fd:
            self.process_file(fd)

### `process_file(self, fd)`

Calculate hashes over 16KiB chuncks of file content.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`fd` | ``str`` | Opened file in read mode. | required

Source code in `torrentfile\hasher.py`

    def process_file(self, fd):
        """Calculate hashes over 16KiB chuncks of file content.

        Parameters
        ----------
        fd : `str`
            Opened file in read mode.
        """
        while True:
            total = 0
            blocks = []
            leaf = bytearray(BLOCK_SIZE)
            # generate leaves of merkle tree

            for _ in range(self.num_blocks):
                size = fd.readinto(leaf)
                total += 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)]
                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()

## `torrentfile.hasher.HasherHybrid (CbMixin)`

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

Source code in `torrentfile\hasher.py`

    class HasherHybrid(CbMixin):
        """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.
        """

        def __init__(self, path, piece_length):
            """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
            logger.debug(
                "Hashing partial Hybrid torrent file. Piece Length: %s Path: %s",
                humanize_bytes(self.piece_length),
                str(self.path),
            )
            with open(path, "rb") as data:
                self._process_file(data)

        def _pad_remaining(self, block_count):
            """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 _process_file(self, data):
            """Calculate layer hashes for contents of file.

            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)
                    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": size,
                        "path": [".pad", str(plength)],
                    }
                    piece.update(bytes(plength))
                self.pieces.append(piece.digest())  # nosec
            self._calculate_root()

        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)

### `__init__(self, path, piece_length)` `special`

Construct Hasher class instances for each file in torrent.

Source code in `torrentfile\hasher.py`

    def __init__(self, path, piece_length):
        """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
        logger.debug(
            "Hashing partial Hybrid torrent file. Piece Length: %s Path: %s",
            humanize_bytes(self.piece_length),
            str(self.path),
        )
        with open(path, "rb") as data:
            self._process_file(data)

## `Edit` Module

module
`torrentfile.` `edit`

Edit torrent meta file.

Functions

- `edit_torrent``(``metafile`, `args``)` — Edit the properties and values in a torrent meta file.
- `filter_empty``(``args`, `meta`, `info``)` — Remove dictionary keys with empty values.

## `torrentfile.edit`

Edit torrent meta file.

### `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

Source code in `torrentfile\edit.py`

    def edit_torrent(metafile, args):
        """
        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.
        """
        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

        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`

    def filter_empty(args, meta, info):
        """
        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]

## `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` — Check a given file or directory to see if it matches a torrentfile.
- `FeedChecker` — Validates torrent content.
- `HashChecker` — Verify that root hashes of content files match the .torrent files.

## `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`

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

Public constructor for Checker class instance.

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:
        """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.
          location (`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, path):
            """Validate data against hashes contained in .torrent file.

            Parameters
            ----------
            metafile : `str`
                path to .torrent file
            path : `str`
                path to content or contents parent directory.
            """
            self.metafile = metafile
            self.meta_version = None
            self.total = 0
            self.paths = []
            self.fileinfo = {}
            self.last_log = None
            if not os.path.exists(metafile):
                raise FileNotFoundError
            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
                else:
                    self.meta_version = 2
            else:
                self.meta_version = 1
            self.root = self.find_root(path)
            self.log_msg("Checking: %s, %s", metafile, 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 hasher(self):
            """Return the hasher class related to torrents meta version.

            Returns
            -------
            `Class[Hasher]`
                the hashing implementation for specific torrent meta version.
            """
            if self.meta_version == 2:
                return HasherV2
            if self.meta_version == 3:
                return HasherHybrid
            return None

        def piece_checker(self):
            """Check individual pieces of the torrent.

            Returns
            -------
            `Obj`
                Individual piece hasher.
            """ ""
            if self.meta_version == 1:
                return FeedChecker
            return HashChecker

        def results(self):
            """Generate result percentage and store for future calls."""
            if self.meta_version == 1:
                iterations = len(self.info["pieces"]) // SHA1
            else:
                iterations = (self.total // self.piece_length) + 1
            responses = []
            for response in tqdm(
                iterable=self.iter_hashes(),
                desc="Calculating",
                total=iterations,
                unit="piece",
            ):
                responses.append(response)
            print(responses)
            return self._result

        def log_msg(self, *args, level=logging.INFO):
            """Log message `msg` to logger and send `msg` to callback hook.

            Parameters
            ----------
            *args : `Iterable`[`str`]
                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):
            """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))
                    self.log_msg("Including file path: %s", 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 = val[""]["pieces root"]
                    length = val[""]["length"]
                    full = str(self.root / base)
                    self.fileinfo[len(self.paths)] = {
                        "path": full,
                        "length": length,
                        "pieces root": roothash,
                    }
                    self.paths.append(full)
                    self.total += length
                    self.log_msg(
                        "Including: path - %s, length - %s",
                        full,
                        humanize_bytes(length),
                    )

                else:
                    self.walk_file_tree(val, partials + [key])

        def iter_hashes(self):
            """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()
            hasher = self.hasher()
            for chunk, piece, path, size in checker(self, hasher):
                consumed += size
                msg = "Match %s: %s %s"
                humansize = humanize_bytes(size)
                matching = 0
                if chunk == piece:
                    matching += size
                    matched += size
                    logger.debug(msg, "Success", path, humansize)
                else:
                    logger.debug(msg, "Fail", path, humansize)
                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,
                )
            self._result = (matched / consumed) * 100 if consumed > 0 else 0

#### `__init__(self, metafile, path)` `special`

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`

    def __init__(self, metafile, path):
        """Validate data against hashes contained in .torrent file.

        Parameters
        ----------
        metafile : `str`
            path to .torrent file
        path : `str`
            path to content or contents parent directory.
        """
        self.metafile = metafile
        self.meta_version = None
        self.total = 0
        self.paths = []
        self.fileinfo = {}
        self.last_log = None
        if not os.path.exists(metafile):
            raise FileNotFoundError
        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
            else:
                self.meta_version = 2
        else:
            self.meta_version = 1
        self.root = self.find_root(path)
        self.log_msg("Checking: %s, %s", metafile, path)
        self.check_paths()

#### `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"],
            }
            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))
                self.log_msg("Including file path: %s", str(self.root / base))
            return
        self.walk_file_tree(self.info["file tree"], [])

#### `find_root(self, 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

Source code in `torrentfile\recheck.py`

    def find_root(self, 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
        ----------
        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)

#### `hasher(self)`

Return the hasher class related to torrents meta version.

Returns:

Type | Description
--- | ---
``Class[Hasher]`` | the hashing implementation for specific torrent meta version.

Source code in `torrentfile\recheck.py`

    def hasher(self):
        """Return the hasher class related to torrents meta version.

        Returns
        -------
        `Class[Hasher]`
            the hashing implementation for specific torrent meta version.
        """
        if self.meta_version == 2:
            return HasherV2
        if self.meta_version == 3:
            return HasherHybrid
        return None

#### `iter_hashes(self)`

Produce results of comparing torrent contents piece by piece.

Returns:

Type | Description
--- | ---
``bytes`` | hash of data found on disk

Source code in `torrentfile\recheck.py`

    def iter_hashes(self):
        """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()
        hasher = self.hasher()
        for chunk, piece, path, size in checker(self, hasher):
            consumed += size
            msg = "Match %s: %s %s"
            humansize = humanize_bytes(size)
            matching = 0
            if chunk == piece:
                matching += size
                matched += size
                logger.debug(msg, "Success", path, humansize)
            else:
                logger.debug(msg, "Fail", path, humansize)
            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,
            )
        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:

Name | Type | Description | Default
--- | --- | --- | ---
`*args` | ``Iterable`[`str`]` | formatting args for log message | `()`
`level` | ``int`` | Log level for this message; default=`logging.INFO` | `20`

Source code in `torrentfile\recheck.py`

    def log_msg(self, *args, level=logging.INFO):
        """Log message `msg` to logger and send `msg` to callback hook.

        Parameters
        ----------
        *args : `Iterable`[`str`]
            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(self)`

Check individual pieces of the torrent.

Returns:

Type | Description
--- | ---
``Obj`` | Individual piece hasher.

Source code in `torrentfile\recheck.py`

    def piece_checker(self):
        """Check individual pieces of the torrent.

        Returns
        -------
        `Obj`
            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`

    @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(self)`

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."""
        if self.meta_version == 1:
            iterations = len(self.info["pieces"]) // SHA1
        else:
            iterations = (self.total // self.piece_length) + 1
        responses = []
        for response in tqdm(
            iterable=self.iter_hashes(),
            desc="Calculating",
            total=iterations,
            unit="piece",
        ):
            responses.append(response)
        print(responses)
        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.

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`

    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 = val[""]["pieces root"]
                length = val[""]["length"]
                full = str(self.root / base)
                self.fileinfo[len(self.paths)] = {
                    "path": full,
                    "length": length,
                    "pieces root": roothash,
                }
                self.paths.append(full)
                self.total += length
                self.log_msg(
                    "Including: path - %s, length - %s",
                    full,
                    humanize_bytes(length),
                )

            else:
                self.walk_file_tree(val, partials + [key])

### `FeedChecker`

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 | `None`

Source code in `torrentfile\recheck.py`

    class FeedChecker:
        """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, hasher=None):
            """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.hasher = hasher
            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):
                self.index = i
                if os.path.exists(path):
                    for piece in self.extract(path, partial):
                        if len(piece) == self.piece_length:
                            yield piece
                        elif 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

        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
            -------
            partial : `bytes`
                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
            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:
                            yield partial
                        break
                    yield partial
                    partial = bytearray(0)
            if length != read:
                for pad in self._gen_padding(partial, length, read):
                    yield pad

        def _gen_padding(self, partial, length, read=0):
            """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, checker, hasher=None)` `special`

Generate hashes of piece length data from filelist contents.

Source code in `torrentfile\recheck.py`

    def __init__(self, checker, hasher=None):
        """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.hasher = hasher
        self.piece_map = {}
        self.index = 0
        self.piece_count = 0
        self.it = None

#### `__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)`

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

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`path` | `str` | path to content. | required
`partial` | `bytearray` | any remaining content from last file. | required

Returns:

Type | Description
--- | ---
`bytearray` | Hash digest for block of .torrent contents.

Source code in `torrentfile\recheck.py`

    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
        -------
        partial : `bytes`
            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
        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:
                        yield partial
                    break
                yield partial
                partial = bytearray(0)
        if length != read:
            for pad in self._gen_padding(partial, length, read):
                yield pad

#### `iter_pieces(self)`

Iterate through, and hash pieces of torrent contents.

Returns:

Type | Description
--- | ---
``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):
            self.index = i
            if os.path.exists(path):
                for piece in self.extract(path, partial):
                    if len(piece) == self.piece_length:
                        yield piece
                    elif 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

### `HashChecker`

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. | `None`

Source code in `torrentfile\recheck.py`

    class HashChecker:
        """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, hasher=None):
            """Construct a HybridChecker instance."""
            self.checker = checker
            self.paths = checker.paths
            self.hasher = hasher
            self.piece_length = checker.piece_length
            self.fileinfo = checker.fileinfo
            self.piece_layers = checker.meta["piece layers"]
            self.piece_count = 0
            self.it = None
            logger.debug(
                "Starting Hash Checker. piece length: %s",
                humanize_bytes(self.piece_length),
            )

        def __iter__(self):
            """Assign iterator and return self."""
            self.it = self.iter_paths()
            return self

        def __next__(self):
            """Provide the result of comparison."""
            try:
                value = next(self.it)
                return value
            except StopIteration as stopiter:
                raise StopIteration() from stopiter

        def iter_paths(self):
            """Iterate through and compare root file hashes to .torrent file.

            Yields
            ------
            results : `tuple`
                The size of the file and result of match.
            """
            for i, path in enumerate(self.paths):
                info = self.fileinfo[i]
                length, plength = info["length"], self.piece_length
                logger.debug("%s length: %s", path, str(length))
                roothash = info["pieces root"]
                logger.debug("%s root hash %s", path, str(roothash))
                if roothash in self.piece_layers:
                    pieces = self.piece_layers[roothash]
                else:
                    pieces = roothash
                amount = len(pieces) // SHA256

                if not os.path.exists(path):
                    for i in range(amount):
                        start = i * SHA256
                        end = start + SHA256
                        piece = pieces[start:end]
                        if length > plength:
                            size = plength
                        else:
                            size = length
                        length -= size
                        block = sha256(bytearray(size)).digest()
                        logging.debug(
                            "Yielding: %s %s %s %s",
                            str(block),
                            str(piece),
                            path,
                            str(size),
                        )
                        yield block, piece, path, size

                else:
                    hashed = self.hasher(path, plength)
                    if len(hashed.layer_hashes) == 1:
                        block = hashed.root
                        piece = roothash
                        size = length
                        yield block, piece, path, size
                    else:
                        for i in range(amount):
                            start = i * SHA256
                            end = start + SHA256
                            piece = pieces[start:end]
                            try:
                                block = hashed.piece_layer[start:end]
                            except IndexError:  # pragma: nocover
                                block = sha256(bytearray(size)).digest()
                            size = plength if plength < length else length
                            length -= size
                            logger.debug(
                                "Yielding: %s, %s, %s, %s",
                                str(block),
                                str(piece),
                                str(path),
                                str(size),
                            )
                            yield block, piece, path, size

#### `__init__(self, checker, hasher=None)` `special`

Construct a HybridChecker instance.

Source code in `torrentfile\recheck.py`

    def __init__(self, checker, hasher=None):
        """Construct a HybridChecker instance."""
        self.checker = checker
        self.paths = checker.paths
        self.hasher = hasher
        self.piece_length = checker.piece_length
        self.fileinfo = checker.fileinfo
        self.piece_layers = checker.meta["piece layers"]
        self.piece_count = 0
        self.it = None
        logger.debug(
            "Starting Hash Checker. piece length: %s",
            humanize_bytes(self.piece_length),
        )

#### `__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_paths()
        return self

#### `__next__(self)` `special`

Provide the result of comparison.

Source code in `torrentfile\recheck.py`

    def __next__(self):
        """Provide the result of comparison."""
        try:
            value = next(self.it)
            return value
        except StopIteration as stopiter:
            raise StopIteration() from stopiter

#### `iter_paths(self)`

Iterate through and compare root file hashes to .torrent file.

Returns:

Type | Description
--- | ---
``tuple`` | The size of the file and result of match.

Source code in `torrentfile\recheck.py`

    def iter_paths(self):
        """Iterate through and compare root file hashes to .torrent file.

        Yields
        ------
        results : `tuple`
            The size of the file and result of match.
        """
        for i, path in enumerate(self.paths):
            info = self.fileinfo[i]
            length, plength = info["length"], self.piece_length
            logger.debug("%s length: %s", path, str(length))
            roothash = info["pieces root"]
            logger.debug("%s root hash %s", path, str(roothash))
            if roothash in self.piece_layers:
                pieces = self.piece_layers[roothash]
            else:
                pieces = roothash
            amount = len(pieces) // SHA256

            if not os.path.exists(path):
                for i in range(amount):
                    start = i * SHA256
                    end = start + SHA256
                    piece = pieces[start:end]
                    if length > plength:
                        size = plength
                    else:
                        size = length
                    length -= size
                    block = sha256(bytearray(size)).digest()
                    logging.debug(
                        "Yielding: %s %s %s %s",
                        str(block),
                        str(piece),
                        path,
                        str(size),
                    )
                    yield block, piece, path, size

            else:
                hashed = self.hasher(path, plength)
                if len(hashed.layer_hashes) == 1:
                    block = hashed.root
                    piece = roothash
                    size = length
                    yield block, piece, path, size
                else:
                    for i in range(amount):
                        start = i * SHA256
                        end = start + SHA256
                        piece = pieces[start:end]
                        try:
                            block = hashed.piece_layer[start:end]
                        except IndexError:  # pragma: nocover
                            block = sha256(bytearray(size)).digest()
                        size = plength if plength < length else length
                        length -= size
                        logger.debug(
                            "Yielding: %s, %s, %s, %s",
                            str(block),
                            str(piece),
                            str(path),
                            str(size),
                        )
                        yield block, piece, path, size

## `Interactive` Module

module
`torrentfile.` `interactive`

Module contains the procedures used for Interactive Mode.

## Functions

`program_Options` gather program behaviour Options.

Classes

- `InteractiveEditor` — Interactive dialog class for torrent editing.
- `InteractiveCreator` — Class namespace for interactive program options.

Functions

- `create_torrent``(``)` — Create new torrent file interactively.
- `edit_action``(``)` — Edit the editable values of the torrent meta file.
- `get_input``(``*args``)` (`str`) — Determine appropriate input function to call.
- `recheck_torrent``(``)` — Check torrent download completed percentage.
- `select_action``(``)` — Operate TorrentFile program interactively through terminal.
- `showcenter``(``txt``)` — Prints text to screen in the center position of the terminal.
- `showtext``(``txt``)` — Print contents of txt to screen.

## `torrentfile.interactive`

Module contains the procedures used for Interactive Mode.

#### Functions

`program_Options` gather program behaviour Options.

### `InteractiveCreator`

Class namespace for interactive program options.

Attributes:

Name | Type | Description
--- | --- | ---
`_piece_length` | `int` | None
`_comment` | `str` | None
`_source` | `str` | None
`_url_list` | `list` | None
`_path` | `str` | None
`_outfile` | `str` | None
`_announce` | `str` | None

Source code in `torrentfile\interactive.py`

    class InteractiveCreator:
        """Class namespace for interactive program options.

        Attributes
        ----------
        _piece_length : int
        _comment : str
        _source : str
        _url_list : list
        _path : str
        _outfile : str
        _announce : str
        """

        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,
            }
            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 list (empty): ", lambda x: isinstance(x, str)
            )
            if url_list:
                self.kwargs["url_list"] = url_list.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__(self)` `special`

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,
        }
        self.outfile, self.meta = self.get_props()

#### `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 list (empty): ", lambda x: isinstance(x, str)
        )
        if url_list:
            self.kwargs["url_list"] = url_list.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`

    class InteractiveEditor:
        """Interactive dialog class for torrent editing."""

        def __init__(self, metafile):
            """
            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),
                "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"]:
                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",
                }
                args = {
                    1: "comment",
                    2: "source",
                    3: "private",
                    4: "announce",
                    5: "url-list",
                }
                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:

Name | Type | Description | Default
--- | --- | --- | ---
`metafile` | ``str`` | user input string identifying the path to a torrent meta file. | required

Source code in `torrentfile\interactive.py`

    def __init__(self, metafile):
        """
        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),
            "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.

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",
            }
            args = {
                1: "comment",
                2: "source",
                3: "private",
                4: "announce",
                5: "url-list",
            }
            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)`

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`

    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"]:
            val = response.split()
        else:
            val = response
        self.args[key] = val

#### `show_current(self)`

Display the current met file information to screen.

Source code in `torrentfile\interactive.py`

    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)

### `create_torrent()`

Create new torrent file interactively.

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

### `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)`

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`

    def get_input(*args):  # 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`

    def recheck_torrent():
        """Check torrent download completed percentage."""
        showcenter("Check Torrent")
        msg = (
            "Enter absolute or relative path to torrent file content, and the "
            "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`

    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 | Edit | Recheck): "
        )
        if action.lower() == "create":
            return create_torrent()
        if "check" in action.lower():
            return recheck_torrent()
        return edit_action()

### `showcenter(txt)`

Prints 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`

    def showcenter(txt):
        """
        Prints 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`

    def showtext(txt):
        """
        Print contents of txt to screen.

        Parameters
        ----------
        txt : `str`
            text to print to terminal.
        """
        sys.stdout.write(txt)

## `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` — Path parameter is required to specify target content.
- `PieceLengthValueError` — Piece Length parameter must equal a perfect power of 2.

Functions

- `filelist_total``(``pathstring``)` (`os.PathLike`) — Perform error checking and format conversion to os.PathLike.
- `get_file_list``(``path``)` (filelist : `list`) — Return a sorted list of file paths contained in directory.
- `get_piece_length``(``size``)` (piece_length : `int`) — Calculate the ideal piece length for bittorrent data.
- `humanize_bytes``(``amount``)` (`str` :) — Convert integer into human readable memory sized denomination.
- `next_power_2``(``value``)` (`int`) — Calculate the next perfect power of 2 equal to or greater than value.
- `normalize_piece_length``(``piece_length``)` (piece_length : `int`) — Verify input piece_length is valid and convert accordingly.
- `path_piece_length``(``path``)` (piece_length : `int`) — Calculate piece length for input path and contents.
- `path_size``(``path``)` (size : `int`) — Return the total size of all files in path recursively.
- `path_stat``(``path``)` (filelist : `list`) — Calculate directory statistics.

## `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.

### `MissingPathError (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` | ``any`` | 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.

        Parameters
        ----------
        message : `any`
            Message for user (optional).
        """

        def __init__(self, message=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__(self, message=None)` `special`

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`

    def __init__(self, message=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 (Exception)`

Piece Length parameter must equal a perfect power of 2.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`message` | ``any`` | Message for user (optional). | `None`

Source code in `torrentfile\utils.py`

    class PieceLengthValueError(Exception):
        """Piece Length parameter must equal a perfect power of 2.

        Parameters
        ----------
        message : `any`
            Message for user (optional).
        """

        def __init__(self, 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.
            """
            self.message = f"Incorrect value for piece length: {str(message)}"
            super().__init__(message)

#### `__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.

Source code in `torrentfile\utils.py`

    def __init__(self, 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.
        """
        self.message = f"Incorrect value for piece length: {str(message)}"
        super().__init__(message)

### `filelist_total(pathstring)`

Perform error checking and format conversion to os.PathLike.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`pathstring` | ``str`` | An existing filesystem path. | required

Exceptions:

Type | Description
--- | ---
`MissingPathError` | File could not be found.

Returns:

Type | Description
--- | ---
``os.PathLike`` | Input path converted to bytes format.

Source code in `torrentfile\utils.py`

    def filelist_total(pathstring):
        """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`

    def get_file_list(path):
        """Return a sorted list of file paths contained in directory.

        Parameters
        ----------
        path : `str`
            target file or directory.

        Returns
        -------
        filelist : `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 peace length size arguement.

Source code in `torrentfile\utils.py`

    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
        -------
        piece_length : `int`
            Ideal peace length size arguement.
        """
        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

Source code in `torrentfile\utils.py`

    def humanize_bytes(amount):
        """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`

    def next_power_2(value):
        """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

Exceptions:

Type | Description
--- | ---
`PieceLengthValueError :` | If piece length is improper value.

Returns:

Type | Description
--- | ---
`int` | normalized piece length.

Source code in `torrentfile\utils.py`

    def normalize_piece_length(piece_length) -> int:
        """Verify input piece_length is valid and convert accordingly.

        Parameters
        ----------
        piece_length : `int` | `str`
            The piece length provided by user.

        Returns
        -------
        piece_length : `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`

    def path_piece_length(path):
        """Calculate piece length for input path and contents.

        Parameters
        ----------
        path : `str`
            The absolute path to directory and contents.

        Returns
        -------
        piece_length : `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`

    def path_size(path):
        """Return the total size of all files in path recursively.

        Parameters
        ----------
        path : `str`
            path to target file or directory.

        Returns
        -------
        size : `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

Source code in `torrentfile\utils.py`

    def path_stat(path):
        """Calculate directory statistics.

        Parameters
        ----------
        path : `str`
            The path to start calculating from.

        Returns
        -------
        filelist : `list`
            List of all files contained in Directory
        size : `int`
            Total sum of bytes from all contents of dir
        piece_length : `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)