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
datadoes 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
cancellationis 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,
Noneto remove, orUNCHANGED. - web_seeds: Web seeds,
Noneto remove, orUNCHANGED. - nodes: DHT nodes,
Noneto remove, orUNCHANGED. - private: Private flag,
Noneto remove, orUNCHANGED. - source: Source string,
Noneto remove, orUNCHANGED. - comment: Comment,
Noneto remove, orUNCHANGED. - created_by: Creator text,
Noneto remove, orUNCHANGED. - creation_date: Unix timestamp,
Noneto remove, orUNCHANGED. - 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_bytesunchanged.
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
encodedwithinoriginal_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.