Python: Convert any .torrent to an I2P-only torrent

All other I2P Bittorrent related talk
ghost92
Posts: 12
Joined: Tue Sep 19, 2023 3:34 am

Python: Convert any .torrent to an I2P-only torrent

Post by ghost92 »

This python script can be used to convert any clearnet torrent into a I2P-only torrent by removing all clearnet trackers and only using I2P trackers.

Code: Select all

#!/usr/bin/env python3

try:
    import bencodepy
except ImportError:
    print("Please install 'bencodepy'")
    exit(1)

from typing import OrderedDict
import argparse
from pathlib import Path
from hashlib import sha1

I2P_TRACKERS = [
    b"http://tracker2.postman.i2p/announce.php",
    b"http://opentracker.r4sas.i2p/a",
    b"http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/announce.php",
    b"http://opentracker.skank.i2p/a",
    b"http://opentracker.dg2.i2p/a",
    b"http://omitracker.i2p/announce.php",
    b"http://w7tpbzncbcocrqtwwm3nezhnnsw4ozadvi2hmvzdhrqzfxfum7wa.b32.i2p/a",
    b"http://tu5skej67ftbxjghnx3r2txp6fqz6ulkolkejc77be2er5v5zrfq.b32.i2p/announce.php",
    b"http://lnQ6yoBTxQuQU8EQ1FlF395ITIQF-HGJxUeFvzETLFnoczNjQvKDbtSB7aHhn853zjVXrJBgwlB9sO57KakBDaJ50lUZgVPhjlI19TgJ-CxyHhHSCeKx5JzURdEW-ucdONMynr-b2zwhsx8VQCJwCEkARvt21YkOyQDaB9IdV8aTAmP~PUJQxRwceaTMn96FcVenwdXqleE16fI8CVFOV18jbJKrhTOYpTtcZKV4l1wNYBDwKgwPx5c0kcrRzFyw5~bjuAKO~GJ5dR7BQsL7AwBoQUS4k1lwoYrG1kOIBeDD3XF8BWb6K3GOOoyjc1umYKpur3G~FxBuqtHAsDRICkEbKUqJ9mPYQlTSujhNxiRIW-oLwMtvayCFci99oX8MvazPS7~97x0Gsm-onEK1Td9nBdmq30OqDxpRtXBimbzkLbR1IKObbg9HvrKs3L-kSyGwTUmHG9rSQSoZEvFMA-S0EXO~o4g21q1oikmxPMhkeVwQ22VHB0-LZJfmLr4SAAAA.i2p/announce.php",
]

I2P_TRACKER_DOMAINS = {
    b"http://tracker2.postman.i2p/announce.php": b"http://6a4kxkg5wp33p25qqhgwl6sj4yh4xuf5b3p3qldwgclebchm3eea.b32.i2p/announce.php",
    b"http://opentracker.r4sas.i2p/a": b"http://punzipidirfqspstvzpj6gb4tkuykqp6quurj6e23bgxcxhdoe7q.b32.i2p/a",
    b"http://opentracker.skank.i2p/a": b"http://by7luzwhx733fhc5ug2o75dcaunblq2ztlshzd7qvptaoa73nqua.b32.i2p/a",
    b"http://opentracker.dg2.i2p/a": b"http://w7tpbzncbcocrqtwwm3nezhnnsw4ozadvi2hmvzdhrqzfxfum7wa.b32.i2p/a",
    b"http://omitracker.i2p/announce.php": b"http://a5ruhsktpdhfk5w46i6yf6oqovgdlyzty7ku6t5yrrpf4qedznjq.b32.i2p/announce.php",
}

POSTMAN_TRACKER = b"http://tracker2.postman.i2p/announce.php"
MAIN_TRACKER = POSTMAN_TRACKER
# MAIN_TRACKER = b'http://punzipidirfqspstvzpj6gb4tkuykqp6quurj6e23bgxcxhdoe7q.b32.i2p/a'


def get_infohash(torrent: OrderedDict) -> str:
    """Calcuate and return the infohash based on given .torrent data"""
    info_encoded = bencodepy.encode(torrent[b"info"])
    _hash = sha1()
    _hash.update(info_encoded)
    infohash = _hash.hexdigest()
    return infohash


def remove_private_flag(torrent: OrderedDict) -> OrderedDict:
    """
    Removes the 'private: 1' flag found in private torrents

    NOTE: This will change the infohash!
    """

    if torrent and torrent.get(b"info") and torrent.get(b"info").get(b"private"):
        del torrent[b"info"][b"private"]
    return torrent


def remove_source_info(torrent: OrderedDict) -> OrderedDict:
    """
    Removes the 'source' field

    NOTE: This will change the infohash!
    """

    if torrent and torrent.get(b"info") and torrent.get(b"info").get(b"source"):
        del torrent[b"info"][b"source"]
    return torrent


def remove_comment(torrent: OrderedDict) -> OrderedDict:
    """Removes the 'comment' field"""

    if torrent and torrent.get(b"comment"):
        del torrent[b"comment"]
    return torrent


def remove_created_by(torrent: OrderedDict) -> OrderedDict:
    """Removes the 'created by' field"""

    if torrent and torrent.get(b"created by"):
        del torrent[b"created by"]
    return torrent


def update_trackers(torrent: OrderedDict) -> OrderedDict:
    """Replace all trackers with I2P-only trackers

    https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure
    """

    torrent[b"announce"] = MAIN_TRACKER
    torrent[b"announce-list"] = [[tracker] for tracker in I2P_TRACKERS]
    return torrent


def write_output(data: OrderedDict, directory: str | Path = "./", filename: str = "out.torrent") -> None:
    """Write torrent file to specified location"""

    output = Path(directory) / Path(filename)
    with open(output, "wb") as file:
        file.write(bencodepy.encode(data))


def should_replace_outfile(filename: Path | str) -> None:
    """Test if output file already exists, if so, ask to replace"""
    if Path(filename).exists() and Path(filename).is_file():
        answer = input(f"{filename} already exists, do you want to overwrite? [y/n]: ")
        return "y" in answer.lower()
    return True


def remove_all_extras(torrent: OrderedDict) -> OrderedDict:
    """Remove all extra metadata from .torrent"""
    remove_comment(torrent)
    remove_created_by(torrent)
    remove_private_flag(torrent)
    remove_source_info(torrent)

    return torrent


def parse_args():
    """Parse arguments given to this program"""
    parser = argparse.ArgumentParser(
        prog="torrent-to-i2p",
        description="Convert clearnet torrent to i2p-only torrent",
        # epilog='Text at the bottom of help'
    )
    parser.add_argument("filename")
    parser.add_argument(
        "-p",
        "--parse",
        default=False,
        action="store_true",
        help="Parse a file and print it for debugging",
    )
    parser.add_argument("-o", "--out", help="Output file to write to")
    parser.add_argument(
        "-d", "--dir", default="./", type=str, help="Output directory to write to"
    )
    args = parser.parse_args()
    return args


def main():
    args = parse_args()

    torrent = bencodepy.decode(open(args.filename, "rb").read())

    if args.parse:
        print("INFOHASH:", get_infohash(torrent))
        print()
        del torrent[b"info"][b"pieces"]
        print(torrent)
        print()
        exit(0)

    out_dir = Path(args.dir)
    if not out_dir.exists():
        out_dir.mkdir(parents=True, exist_ok=True)

    out_name = args.out or Path(args.filename).name
    if not should_replace_outfile(out_dir / Path(out_name)):
        exit(0)

    pre_infohash = get_infohash(torrent)
    remove_all_extras(torrent)
    update_trackers(torrent)
    post_infohash = get_infohash(torrent)

    if pre_infohash != post_infohash:
        print("[!]", args.filename, "info hash changed, beware!")

    write_output(torrent, directory=args.dir, filename=out_name)


if __name__ == "__main__":
    main()

Example Use:

Code: Select all

$ ./torrent-to-i2p.py -p ubuntu-23.10.1-desktop-amd64.iso.torrent
INFOHASH: 9ecd4676fd0f0474151a4b74a5958f42639cebdf

OrderedDict([(b'announce', b'https://torrent.ubuntu.com/announce'), (b'announce-list', [[b'https://torrent.ubuntu.com/announce'], [b'https://ipv6.torrent.ubuntu.com/announce']]), (b'comment', b'Ubuntu CD releases.ubuntu.com'), (b'created by', b'mktorrent 1.1'), (b'creation date', 1697466120), (b'info', OrderedDict([(b'length', 5173995520), (b'name', b'ubuntu-23.10.1-desktop-amd64.iso'), (b'piece length', 262144)]))])

$ ./torrent-to-i2p.py ./ubuntu-23.10.1-desktop-amd64.iso.torrent
ubuntu-23.10.1-desktop-amd64.iso.torrent already exists, do you want to overwrite? [y/n]: y
$ ./torrent-to-i2p.py -p ubuntu-23.10.1-desktop-amd64.iso.torrent
INFOHASH: 9ecd4676fd0f0474151a4b74a5958f42639cebdf

OrderedDict([(b'announce', b'http://tracker2.postman.i2p/announce.php'), (b'announce-list', [[b'http://tracker2.postman.i2p/announce.php'], [b'http://opentracker.r4sas.i2p/a'], [b'http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/announce.php'], [b'http://opentracker.skank.i2p/a'], [b'http://opentracker.dg2.i2p/a'], [b'http://omitracker.i2p/announce.php'], [b'http://w7tpbzncbcocrqtwwm3nezhnnsw4ozadvi2hmvzdhrqzfxfum7wa.b32.i2p/a'], [b'http://tu5skej67ftbxjghnx3r2txp6fqz6ulkolkejc77be2er5v5zrfq.b32.i2p/announce.php'], [b'http://lnQ6yoBTxQuQU8EQ1FlF395ITIQF-HGJxUeFvzETLFnoczNjQvKDbtSB7aHhn853zjVXrJBgwlB9sO57KakBDaJ50lUZgVPhjlI19TgJ-CxyHhHSCeKx5JzURdEW-ucdONMynr-b2zwhsx8VQCJwCEkARvt21YkOyQDaB9IdV8aTAmP~PUJQxRwceaTMn96FcVenwdXqleE16fI8CVFOV18jbJKrhTOYpTtcZKV4l1wNYBDwKgwPx5c0kcrRzFyw5~bjuAKO~GJ5dR7BQsL7AwBoQUS4k1lwoYrG1kOIBeDD3XF8BWb6K3GOOoyjc1umYKpur3G~FxBuqtHAsDRICkEbKUqJ9mPYQlTSujhNxiRIW-oLwMtvayCFci99oX8MvazPS7~97x0Gsm-onEK1Td9nBdmq30OqDxpRtXBimbzkLbR1IKObbg9HvrKs3L-kSyGwTUmHG9rSQSoZEvFMA-S0EXO~o4g21q1oikmxPMhkeVwQ22VHB0-LZJfmLr4SAAAA.i2p/announce.php']]), (b'creation date', 1697466120), (b'info', OrderedDict([(b'length', 5173995520), (b'name', b'ubuntu-23.10.1-desktop-amd64.iso'), (b'piece length', 262144)]))])