Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Metainfo API

btpc.metainfo

Parse, inspect, validate, edit, serialize, and verify torrent metainfo.

BencodeDictionary

class BencodeDictionary(items: tuple[tuple[bytes, BencodeValue], ...])

Store an immutable bencode dictionary in canonical raw-key order.

Iteration yields keys in canonical order. Indexing, membership, get, keys, and values provide dictionary-style access. The items attribute remains an immutable tuple of entries for compatibility.

Attributes:

  • items: Unique raw-byte keys and recursive values.

items

items: tuple[tuple[bytes, BencodeValue], ...]

get

get(key: bytes, default: _DefaultValue | None = None) -> BencodeValue | _DefaultValue | None

Return the value for a raw key, or the supplied default when absent.

keys

keys() -> tuple[bytes, ...]

Return raw keys in canonical order.

values

values() -> tuple[BencodeValue, ...]

Return values in canonical key order.

BencodeList

class BencodeList(values: tuple[BencodeValue, ...])

Store an immutable ordered bencode list.

Attributes:

  • values: Recursive values in source order.

values

values: tuple[BencodeValue, ...]

BencodeValue

BencodeValue: TypeAlias

Metainfo

class Metainfo

Represent validated metainfo while preserving its exact source identity.

Parsing retains the original metainfo bytes and computes info hashes from the exact raw info dictionary slice, not from re-serialization. Equality also compares exact original bytes. Use write to save while preserving info hashes, to_bytes for canonical output, and original_bytes when byte-for-byte source identity is required. Raw protocol strings and paths remain bytes; optional *_text views decode only valid UTF-8.

Examples:

>>> from btpc import Metainfo, TorrentMode
>>> data = (
...     b"d4:infod6:lengthi1e4:name1:x12:piece lengthi16384e"
...     b"6:pieces20:00000000000000000000ee"
... )
>>> torrent = Metainfo.from_bytes(data)
>>> torrent.mode is TorrentMode.V1
True
>>> torrent.original_bytes == data
True

from_bytes

from_bytes(data: object, *, options: ParseOptions | None = None) -> Metainfo

Parse metainfo from a contiguous Python buffer.

Args:

  • data: bytes, bytearray, or another readable contiguous buffer. Non-contiguous memoryviews are rejected.
  • options: Parser input, allocation, and integer-digit limits.

Returns:

A validated immutable metainfo object retaining exact source bytes.

Raises:

  • TypeError: If data does not provide a contiguous buffer.
  • BencodeError: If bencode syntax is invalid.
  • MetainfoError: If decoded fields violate torrent protocol rules.
  • ResourceLimitError: If a configured parse limit is exceeded.

Examples:

>>> from btpc import Metainfo
>>> data = (
...     b"d4:infod6:lengthi1e4:name1:x12:piece lengthi16384e"
...     b"6:pieces20:00000000000000000000ee"
... )
>>> Metainfo.from_bytes(memoryview(data)).name
b'x'

read

read(path: str | PathLike[str], *, options: ParseOptions | None = None) -> Metainfo

Read and parse metainfo directly from a filesystem path.

Args:

  • path: Torrent file path.
  • options: Parser input, allocation, and integer-digit limits.

Returns:

A validated immutable metainfo object.

Raises:

  • PathError: If the file cannot be opened or read.
  • BencodeError: If bencode syntax is invalid.
  • MetainfoError: If decoded fields violate torrent protocol rules.
  • ResourceLimitError: If a configured parse limit is exceeded.

Examples:

>>> from pathlib import Path
>>> from tempfile import TemporaryDirectory
>>> from btpc import Metainfo
>>> data = (
...     b"d4:infod6:lengthi1e4:name1:x12:piece lengthi16384e"
...     b"6:pieces20:00000000000000000000ee"
... )
>>> with TemporaryDirectory() as directory:
...     path = Path(directory) / "sample.torrent"
...     _ = path.write_bytes(data)
...     assert Metainfo.read(path).original_bytes == data

verify

verify(payload: str | PathLike[str], *, fail_fast: bool = False, extra_files: bool = False, progress: Callable[[int, int, int], None] | None = None, cancellation: CancellationToken | None = None) -> PayloadVerificationReport

Verify payload files using every hash domain present in this torrent.

payload is resolved as the torrent’s payload root: a single-file torrent may point directly to its file, while a multi-file torrent points to the containing directory. v1 verifies the concatenated piece stream, v2 verifies per-file Merkle roots, and hybrid verifies both. Mismatches are returned in deterministic order; filesystem and policy failures raise exceptions.

Args:

  • payload: Payload file or root directory.
  • fail_fast: Stop after the first mismatch when true.
  • extra_files: Report files not represented by the torrent.
  • progress: Optional callback receiving ``(completed_bytes, total_bytes, completed_pieces)`. Callback exceptions propagate unchanged.
  • cancellation: Cooperative cancellation token.

Returns:

A report containing zero or more deterministic mismatches.

Raises:

  • PathError: If required payload paths cannot be read safely.
  • VerificationError: If verification cannot be performed as requested.
  • CancelledError: If cancellation is requested.
  • Exception: Any exception raised by progress.

Examples:

>>> from pathlib import Path
>>> from tempfile import TemporaryDirectory
>>> from btpc import CreateOptions, Metainfo, create_bytes
>>> with TemporaryDirectory() as directory:
...     payload = Path(directory) / "hello.txt"
...     _ = payload.write_bytes(b"hello torrent\n")
...     created = create_bytes(
...         payload,
...         options=CreateOptions(creation_date=0, threads=1),
...     )
...     assert Metainfo.from_bytes(created.bytes).verify(payload).is_valid

magnet

magnet(*, display_name: bool = True, trackers: bool = True, web_seeds: bool = True) -> str

Generate a deterministic magnet URI.

Args:

  • display_name: Include the decoded display name when valid UTF-8.
  • trackers: Include tracker URLs.
  • web_seeds: Include web seed URLs.

Returns:

A magnet URI containing every applicable v1 and v2 exact-topic hash.

Examples:

>>> from btpc import Metainfo
>>> data = (
...     b"d4:infod6:lengthi1e4:name1:x12:piece lengthi16384e"
...     b"6:pieces20:00000000000000000000ee"
... )
>>> magnet = Metainfo.from_bytes(data).magnet(trackers=False)
>>> magnet.startswith("magnet:?xt=")
True

edit

edit(*, trackers: Sequence[Sequence[str]] | None | UnchangedType = UNCHANGED, web_seeds: Sequence[str] | None | UnchangedType = UNCHANGED, nodes: Sequence[tuple[str, int]] | None | UnchangedType = UNCHANGED, private: bool | None | UnchangedType = UNCHANGED, source: str | None | UnchangedType = UNCHANGED, comment: str | None | UnchangedType = UNCHANGED, created_by: str | None | UnchangedType = UNCHANGED, creation_date: int | None | UnchangedType = UNCHANGED, raw_top_level: Mapping[bytes, BencodeValue] | None = None, file_attributes: dict[tuple[bytes, ...], bytes] | None = None) -> Metainfo

Return a validated copy with explicit preserve, remove, or set edits.

Optional fields use three states: :data:btpc.UNCHANGED preserves the current value, None removes it, and a typed value replaces it. Trackers, web seeds, nodes, comments, creator, and creation date are top-level fields and do not affect info hashes. private, source, and file attributes edit the info dictionary and therefore change applicable info hashes. Top-level-only edits retain the exact original info bytes, including a noncanonical source encoding. Use to_bytes for an explicit fully canonical serialization.

Args:

  • trackers: Tracker tiers, None to remove, or UNCHANGED.
  • web_seeds: Web seeds, None to remove, or UNCHANGED.
  • nodes: DHT nodes, None to remove, or UNCHANGED.
  • private: Private flag, None to remove, or UNCHANGED.
  • source: Source string, None to remove, or UNCHANGED.
  • comment: Comment, None to remove, or UNCHANGED.
  • created_by: Creator text, None to remove, or UNCHANGED.
  • creation_date: Unix timestamp, None to remove, or UNCHANGED.
  • raw_top_level: Raw recursive bencode extension replacements.
  • file_attributes: Raw attributes keyed by raw torrent path components.

Returns:

A newly validated immutable object retaining untouched info bytes.

Raises:

  • TypeError: If a textual field receives a non-string value.
  • MetainfoError: If an edit would produce invalid metainfo.

Examples:

>>> from btpc import UNCHANGED, Metainfo
>>> data = (
...     b"d4:infod6:lengthi1e4:name1:x12:piece lengthi16384e"
...     b"6:pieces20:00000000000000000000ee"
... )
>>> torrent = Metainfo.from_bytes(data)
>>> with_comment = torrent.edit(comment="reviewed")
>>> preserved = with_comment.edit(comment=UNCHANGED)
>>> removed = with_comment.edit(comment=None)
>>> preserved.info_hash_v1 == removed.info_hash_v1 == torrent.info_hash_v1
True
>>> torrent.edit(source="release").info_hash_v1 != torrent.info_hash_v1
True

mode

mode: TorrentMode

Return the validated protocol representation.

name

name: bytes

Return raw torrent name bytes.

piece_length

piece_length: int

Return piece length in bytes.

total_length

total_length: int

Return total payload bytes.

piece_count

piece_count: int

Return logical piece count.

files

files: tuple[TorrentFile, ...]

Return cached immutable payload file objects.

trackers

trackers: tuple[tuple[bytes, ...], ...]

Return cached immutable tracker tiers.

trackers_text

trackers_text: tuple[tuple[str, ...], ...] | None

Return decoded tracker tiers, or None if any URL is not UTF-8.

web_seeds

web_seeds: tuple[bytes, ...]

Return cached immutable web seeds.

web_seeds_text

web_seeds_text: tuple[str, ...] | None

Return decoded web seeds, or None if any URL is not UTF-8.

private

private: bool | None

Return the explicit private flag.

nodes

nodes: tuple[tuple[bytes, int], ...]

Return cached immutable DHT bootstrap nodes as raw hosts and ports.

nodes_text

nodes_text: tuple[tuple[str, int], ...] | None

Return decoded DHT nodes, or None if any host is not UTF-8.

source

source: bytes | None

Return raw source bytes from the info dictionary.

source_text

source_text: str | None

Return the source as UTF-8, or None when absent or undecodable.

comment

comment: bytes | None

Return raw top-level comment bytes.

comment_text

comment_text: str | None

Return the comment as UTF-8, or None when absent or undecodable.

created_by

created_by: bytes | None

Return raw top-level creator bytes.

created_by_text

created_by_text: str | None

Return creator text as UTF-8, or None when absent or undecodable.

creation_date

creation_date: int | None

Return the non-negative Unix creation timestamp.

info_hash_v1

info_hash_v1: HashValue | None

Return the cached v1 info hash when applicable.

info_hash_v2

info_hash_v2: HashValue | None

Return the cached v2 info hash when applicable.

original_bytes

original_bytes: bytes

Return exact source bytes, copied only on first request.

unknown_fields

unknown_fields: tuple[UnknownField, ...]

Return cached unknown fields with values and exact source encodings.

name_text

name_text: str | None

Return the name as UTF-8 when valid.

write

write(destination: str | PathLike[str], *, canonical: bool = False, overwrite: bool = False, durable: bool = False) -> None

Atomically save metainfo while preserving its info hashes by default.

Default output retains this object’s exact bytes, including edits already applied. canonical=True normalizes bencoding and can change info hashes when the source info dictionary is noncanonical. Payload files are not read.

Args:

  • destination: Torrent file to publish.
  • canonical: Normalize all bencoding when true. The default preserves this object’s encoding and torrent identity.
  • overwrite: Replace an existing destination when true.
  • durable: Request file and parent-directory synchronization.

Raises:

  • PathError: If publication fails or overwrite is disallowed. A directory synchronization failure can occur after complete publication.

Examples:

>>> from pathlib import Path
>>> from tempfile import TemporaryDirectory
>>> from btpc import Metainfo
>>> source = (
...     b"d4:infod6:lengthi00e4:name1:x12:piece lengthi16384e6:pieces0:ee"
... )
>>> torrent = Metainfo.from_bytes(source)
>>> with TemporaryDirectory() as directory:
...     path = Path(directory) / "reviewed.torrent"
...     torrent.edit(comment="reviewed").write(path)
...     assert Metainfo.read(path).info_hash_v1 == torrent.info_hash_v1

to_bytes

to_bytes(*, canonical: bool = True) -> bytes

Serialize canonical metainfo or return the exact original bytes.

The compatibility default is canonical output. For noncanonical source info this can change info hashes on reparse. Use canonical=False or write to preserve this object’s torrent identity.

Args:

  • canonical: Sort dictionary keys by unsigned raw-byte order and normalize bencode when true. False returns original_bytes unchanged.

Returns:

Serialized metainfo bytes.

Examples:

>>> from btpc import Metainfo
>>> data = (
...     b"d4:infod6:lengthi1e4:name1:x12:piece lengthi16384e"
...     b"6:pieces20:00000000000000000000ee"
... )
>>> torrent = Metainfo.from_bytes(data)
>>> torrent.to_bytes(canonical=False) == data
True
>>> reparsed = Metainfo.from_bytes(torrent.to_bytes())
>>> reparsed.info_hash_v1 == torrent.info_hash_v1
True

validation

validation: ValidationReport

Return the construction-time validation report.

validate

validate() -> ValidationReport

Return validation for compatibility; no new check is performed.

TorrentFile

class TorrentFile(length: int, path: tuple[bytes, ...], attributes: bytes, pieces_root: HashValue | None, is_padding: bool = False)

Describe one file in the torrent’s logical payload.

Attributes:

  • length: File length in bytes.
  • path: Raw torrent path components. Components remain bytes because torrent paths are not required to be UTF-8.
  • attributes: Raw BEP 47/BEP 52 file attribute bytes.
  • pieces_root: v2 Merkle root when present.
  • is_padding: Whether the entry is a padding file.

length

length: int

path

path: tuple[bytes, ...]

attributes

attributes: bytes

pieces_root

pieces_root: HashValue | None

is_padding

is_padding: bool

path_text

path_text: tuple[str, ...] | None

Return UTF-8 components when every component decodes.

torrent_path

torrent_path: TorrentPath

Return the raw-identity typed torrent path.

UnknownField

class UnknownField(key: bytes, value: BencodeValue, encoded: bytes, span: tuple[int, int])

Describe one unknown top-level field and its exact source encoding.

Attributes:

  • key: Raw top-level dictionary key.
  • value: Recursive semantic bencode value.
  • encoded: Exact source bytes covering the encoded key and value.
  • span: Half-open offsets for encoded within original_bytes.

key

key: bytes

value

value: BencodeValue

encoded

encoded: bytes

span

span: tuple[int, int]

ValidationReport

class ValidationReport(warnings: tuple[str, ...], canonical: bool, canonical_offset: int | None = None, canonical_message: str | None = None)

Summarize validation and canonical-encoding status.

Attributes:

  • warnings: Non-fatal protocol or interoperability warnings.
  • canonical: Whether the original source used canonical bencoding.
  • canonical_offset: Source offset of the first canonicalization issue.
  • canonical_message: Human-readable canonicalization issue.

warnings

warnings: tuple[str, ...]

canonical

canonical: bool

canonical_offset

canonical_offset: int | None

canonical_message

canonical_message: str | None

is_valid

is_valid: bool

Return true because invalid metainfo fails construction.