torrent-models

Welcome, you have reached a website that is about a software program for using bittorrent .torrent files.

Tip

If you intended to be on a different website, please consult the world wide web on the browser for your device.


This program is about using pydantic data models to create, edit, and extend .torrent files.

While there are many other torrent packages, this one:

  • Is simple and focused

  • Can create and parse v1, v2, hybrid, and other BEPs

  • Is focused on library usage (but does cli things too)

  • Validates torrent files (e.g. when accepting them as user input!)

  • Treats .torrent files as an extensible rather than fixed format

  • Is performant! (and asyncio compatible when hashing!)

  • Uses python typing and is mypy friendly

this package was created for, and used by sciop <3

Examples

Read a torrent

from torrent_models import Torrent

torrent = Torrent.read("tentacoli-1977-ost.torrent")
torrent.pprint(verbose=1)   
                      tentacles-tentacoli-1977-ost-soundtrack                      
┌──────────────┬──────────────────────────────────────────────────────────────────┐
│ # Files      │ 21                                                               │
│ Total Size   │ 57.3 MiB                                                         │
│ Piece Size   │ 2.0 MiB                                                          │
│ Torrent Size │ 7.8 KiB                                                          │
│ V1 Infohash  │ 15590c30a2ffe45e7b2cf17c90256adec1d638dc                         │
│ V2 Infohash  │ 1bebedb6d2f396ba4e22fc2cc317987be244062be177419d5094716de8422194 │
└──────────────┴──────────────────────────────────────────────────────────────────┘
                                            Files                                            
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Path                                                                Size       Hash     ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━┩
│ tentacoli-01-Small Town Pleasures.mp3                              │ 3.0 MiB   │ e755700c │
│ tentacoli-02-She'll Never Come Back.mp3                            │ 2.7 MiB   │ d1c3d80b │
│ tentacoli-03-My Son's Friend Is A Champion Pisser.mp3              │ 2.2 MiB   │ 3b458ec6 │
│ tentacoli-04-Summer And Winter.mp3                                 │ 2.4 MiB   │ 3ff20c6f │
│ tentacoli-05-San Diego, Yellow Cab.mp3                             │ 2.7 MiB   │ 7ad85e14 │
│ tentacoli-06-Happiness Is Having Two Killer Whales As Friends.mp3  │ 3.7 MiB   │ d0063c2d │
│ tentacoli-07-Too Risky A Day For A Regatta.mp3                     │ 3.5 MiB   │ 4767dc7f │
│ tentacoli-08-Sorry, I Have To Go.mp3                               │ 2.5 MiB   │ 661b7dc0 │
│ tentacoli-09-Scotch For Two.mp3                                    │ 1.8 MiB   │ 6bdfbd14 │
│ tentacoli-10-The Killer Whales' Games.mp3                          │ 2.3 MiB   │ e5273e3a │
│ tentacoli-11-The Capture Of The Giant Octopus.mp3                  │ 1.7 MiB   │ 91b3f6f3 │
│ tentacoli-12-Two Old Kids.mp3                                      │ 2.8 MiB   │ 0b09f31a │
│ tentacoli-13-Tentacles.mp3                                         │ 4.1 MiB   │ 26c6744b │
│ tentacoli-14-My Son's Friend Is A Champion Pisser (Versione 2).mp3 │ 1.5 MiB   │ 736490ac │
│ tentacoli-15-Tentacles (Versione 2).mp3                            │ 5.0 MiB   │ 4c94d4d2 │
│ tentacoli-16-San Diego, Yellow Cab (Versione 2).mp3                │ 2.7 MiB   │ b9f50e8b │
│ tentacoli-17-My Son's Friend Is A Champion Pisser (Versione 3).mp3 │ 1.0 MiB   │ a2781d81 │
│ tentacoli-18-Too Risky A Day For A Regatta (Versione 2).mp3        │ 854.5 KiB │ c67de66b │
│ tentacoli-19-Tentacles (Versione 3).mp3                            │ 3.8 MiB   │ b6145263 │
│ tentacoli-20-Sails.mp3                                             │ 4.2 MiB   │ 72436d6a │
│ tentacoli-21-Sails (Versione 2).mp3                                │ 2.7 MiB   │ b27df273 │
└────────────────────────────────────────────────────────────────────┴───────────┴──────────┘
{
    'announce': 'udp://example.com:6969',
    'comment': "it's the best OST of all time for absolutely no reason",
    'created by': 'qBittorrent v5.0.4',
    'creation date': 1748082221,
    'info': {
        'name': 'tentacles-tentacoli-1977-ost-soundtrack',
        'source': 'i forgot at this point',
        'piece length': 2097152,
        'meta version': 2
    },
    'url-list': 'https://example.com/files'
}

Edit a torrent

It’s just a normal pydantic model!

from rich import print
print(torrent)

Hide code cell output

Torrent(
    announce='udp://example.com:6969',
    announce_list=None,
    comment="it's the best OST of all time for absolutely no reason",
    created_by='qBittorrent v5.0.4',
    creation_date=datetime.datetime(2025, 5, 24, 10, 23, 41),
    info=InfoDictHybrid(
        name='tentacles-tentacoli-1977-ost-soundtrack',
        source='i forgot at this point',
        pieces=[
            b'=\x94\x0c\x07\xfe\x86S\xa2E5\xaf\xd1\x14\xc2l\x0b\xe9\x93r%',
            b"\xbd\xed\xab\x0b9\x8a\x05\xaf\x99\x18'6\xdf\xb4\xef\xd1EC\x07\x15",
            b'\x9d\xf11\xdfm\xf7\x98\xc0ZNu7ks\xe5\xe7\xbcIi\xe0',
            b'Y\xb3J]\xd4y\xc0\xb9\xb8"\xc3m\xf2\x15\xa1\xd52\x9aMr',
            b'\xd6\xa3\x19i\xe3\x89P\xa8\x06\x06\xe6\x8e`\x16)w\xd7o\xd3\x05',
            b'E\xcfLP\xae\x97\xb3\xe5\xd9g\x08@\x88\xbb\x91\xbc\x07\x82/\x8d',
            b'o\x9bUD\x9d\x8d\xa93\xab\xa3\xdf\xa9\x11\xf1\x98O\x99\xbd\xefu',
            b'\x87e\xc0\xa6P\x1d^\xeb\xd3\\\x98?2\xf5V\x07\xc6\xd5\x8c\x1b',
            b'\x17;\xd72\xc9\x1d.e\x06.D\rxz\xc1\x83\x00\x9e\x1c\xb4',
            b'\\\x18\xf9\x1d?,\x94\xab4\xa7]\xb5\x0bh\xf9\xe6\xb2\xf6"&',
            b'\x032>\xaaay\x8d\x1bE\x86"\xd9\xc2hR?\xd3n ?',
            b'\xd0\xd8\x18\x1cCZ|\xdb}\xd1\n\\\x8c\xae\xf58\x8a\xadu\x08',
            b'\x91\\\x8c\x17\xe6iDnl\x8e\xce2\xcf\x1b\x14}\xfc\xe5Oq',
            b'\xa6\x0e\xdc\xb1u5\xc0\xfb\xd0\tB\xab\x830\xa5\x7f\x1bA\xe1\x90',
            b'\xebr\xfd\xbf\xbb8\xdb\xea\x1a3\xb1\xc5$\x8a\xa5\x07R\xb3Y|',
            b'\xfb\x1e~\xd7zRb\x04\xe4\xa4\xa5\x98\xf2d\xf0\xb3[\x19V\x7f',
            b'/\xa1\x00\xa8\x9fx\x11\x8b\xa8\xaa\x9f\xd7\xec\xae@^\xbd\xcd\xbd\xd8',
            b'\xa0\xe8H\xde\xee\x88\xb7\xc8Y\xbf&\x1eG\xaf7\xe4\x8f\xd05j',
            b'\xd2EM\x97\x92[\x9d?\xc0\xa3\xa8\x8f\xd4\xadp\xd1\x8f8\x1f/',
            b'\xe2VZ\x90\x16\x88\x8a\x9e\xfb\x89c\xb8\x1au)b\x8fL<7',
            b'\x19\xda\x85\x8f\x18p1\x93.Ij\x1c\x12\xe93\x87Z;\xf41',
            b'O\x07\x02\x00\x91q{K\xfc\x94\xe7\xe2\x0f\x00\x89\xab(q\x1c*',
            b'!\xd48\xbf\x0f\xd1\x1d##/\xf4Kqt<\xda\x92\x0fp\xb1',
            b'#fZv\xeb`6oP\xa0\xc6e&\xee\xd3}M\x07\xa00',
            b'\x7f2@lZ\x08\xd9\xdb\xa5Z\xed\x93b\x18\x97\xef\xdd\x81W\x96',
            b'[]!d\xac\x8c@s\xeel\xa8\xa6\xf7\xb1\xf7\x8d\xa32\xac\xe8',
            b'\xe0F\x1e\x83\xban\x1a\xfef\x14\xee\x8e\xf4\x9f\xef{\x99\xe4\xc9\xc2',
            b'\x96\xa6\x05\x82\xd5\xd8\x16\x89z:\x080SY\x12RP\x14\xfb\xb1',
            b'{\xc0m\xa7XaVBO\xef\x13\xf1\x02S\xbe&\x06\xc4!8',
            b'\xe9/\x99\x9c_D\xb5\xc5\x19\xe9*\x0e<q\x8c\xfbCc\x1b7',
            b'\xf5JF+\x85XUk3v\x0e\xc3-\xd6\xea\x0f\x14\xc9]\xc6',
            b'p\xe1\xf5p]!%WYI\x17\x04\xd1\xa6\x1c\xe2\xe4\x0b\xfe3',
            b'j"\xb0\x19R\x12\xf9\x03\xbd\xc1p\xf6B\x8dP\xe7\xdd\x0e.\xef',
            b'<n\x8d\x9cWk\xa0U\x01@\xb2\xae\x8bx\x06MK\x00\x07h',
            b'H?H\xa3j\xabJo\xd4}\xad\xb2\xe5\x1e\xc4\xd1\xe7\xfa.\xfc',
            b'\xe40 \xd6:\xb20\xcd\x87I\xfd$lQ\xe7@ND\xc6\n',
            b'TI">\xb2\xe4\x82TqY)\x1e\x7f\xdb\x98\xdfE\xf1\xe64',
            b'\xc8\x07\x82\xdf\xd0\xe6~=\xf8\x88\xb8\xd8\x00.\x15Y\xe1\xaa\x94\xe8',
            b'\xaa,\xb0\xd9\x8c\x1e\x9b\x80\xa8\x9a\xa8\x90\xbc\x0c\xd6\x0bT\x15L\xc3',
            b'\xa3\xdf-\x81\xb52\xe1\\_T+\x11\x96\xb4\xfa\x8b\xf3\xcf\x9c\xe2'
        ],
        length=None,
        files=[
            FileItem(length=3177585, path=['tentacoli-01-Small Town Pleasures.mp3'], attr=None),
            FileItem(length=1016719, path=['.pad', '1016719'], attr=b'p'),
            FileItem(length=2778997, path=["tentacoli-02-She'll Never Come Back.mp3"], attr=None),
            FileItem(length=1415307, path=['.pad', '1415307'], attr=b'p'),
            FileItem(length=2333412, path=["tentacoli-03-My Son's Friend Is A Champion Pisser.mp3"], attr=None),
            FileItem(length=1860892, path=['.pad', '1860892'], attr=b'p'),
            FileItem(length=2562622, path=['tentacoli-04-Summer And Winter.mp3'], attr=None),
            FileItem(length=1631682, path=['.pad', '1631682'], attr=b'p'),
            FileItem(length=2796486, path=['tentacoli-05-San Diego, Yellow Cab.mp3'], attr=None),
            FileItem(length=1397818, path=['.pad', '1397818'], attr=b'p'),
            FileItem(
                length=3863849,
                path=['tentacoli-06-Happiness Is Having Two Killer Whales As Friends.mp3'],
                attr=None
            ),
            FileItem(length=330455, path=['.pad', '330455'], attr=b'p'),
            FileItem(length=3696387, path=['tentacoli-07-Too Risky A Day For A Regatta.mp3'], attr=None),
            FileItem(length=497917, path=['.pad', '497917'], attr=b'p'),
            FileItem(length=2583746, path=['tentacoli-08-Sorry, I Have To Go.mp3'], attr=None),
            FileItem(length=1610558, path=['.pad', '1610558'], attr=b'p'),
            FileItem(length=1924024, path=['tentacoli-09-Scotch For Two.mp3'], attr=None),
            FileItem(length=173128, path=['.pad', '173128'], attr=b'p'),
            FileItem(length=2463949, path=["tentacoli-10-The Killer Whales' Games.mp3"], attr=None),
            FileItem(length=1730355, path=['.pad', '1730355'], attr=b'p'),
            FileItem(length=1783901, path=['tentacoli-11-The Capture Of The Giant Octopus.mp3'], attr=None),
            FileItem(length=313251, path=['.pad', '313251'], attr=b'p'),
            FileItem(length=2965602, path=['tentacoli-12-Two Old Kids.mp3'], attr=None),
            FileItem(length=1228702, path=['.pad', '1228702'], attr=b'p'),
            FileItem(length=4319196, path=['tentacoli-13-Tentacles.mp3'], attr=None),
            FileItem(length=1972260, path=['.pad', '1972260'], attr=b'p'),
            FileItem(
                length=1555839,
                path=["tentacoli-14-My Son's Friend Is A Champion Pisser (Versione 2).mp3"],
                attr=None
            ),
            FileItem(length=541313, path=['.pad', '541313'], attr=b'p'),
            FileItem(length=5261174, path=['tentacoli-15-Tentacles (Versione 2).mp3'], attr=None),
            FileItem(length=1030282, path=['.pad', '1030282'], attr=b'p'),
            FileItem(length=2877921, path=['tentacoli-16-San Diego, Yellow Cab (Versione 2).mp3'], attr=None),
            FileItem(length=1316383, path=['.pad', '1316383'], attr=b'p'),
            FileItem(
                length=1079679,
                path=["tentacoli-17-My Son's Friend Is A Champion Pisser (Versione 3).mp3"],
                attr=None
            ),
            FileItem(length=1017473, path=['.pad', '1017473'], attr=b'p'),
            FileItem(
                length=874993,
                path=['tentacoli-18-Too Risky A Day For A Regatta (Versione 2).mp3'],
                attr=None
            ),
            FileItem(length=1222159, path=['.pad', '1222159'], attr=b'p'),
            FileItem(length=3951350, path=['tentacoli-19-Tentacles (Versione 3).mp3'], attr=None),
            FileItem(length=242954, path=['.pad', '242954'], attr=b'p'),
            FileItem(length=4358356, path=['tentacoli-20-Sails.mp3'], attr=None),
            FileItem(length=1933100, path=['.pad', '1933100'], attr=b'p'),
            FileItem(length=2870766, path=['tentacoli-21-Sails (Versione 2).mp3'], attr=None),
            FileItem(length=1323538, path=['.pad', '1323538'], attr=b'p')
        ],
        piece_length=2097152,
        meta_version=2,
        file_tree={
            b'tentacoli-01-Small Town Pleasures.mp3': {
                '': {
                    'length': 3177585,
                    'pieces root': 
b'\xe7Up\x0c[\xce\xa4\x90Z\x1a?\x90\x03Q\xd8\xa5V@\x98\xbc\x08\x1egT\xe3y\xf8\x05r\x8d\x19\n'
                }
            },
            b"tentacoli-02-She'll Never Come Back.mp3": {
                '': {
                    'length': 2778997,
                    'pieces root': 
b'\xd1\xc3\xd8\x0b\xf1?\xd4+>\x85\x82\xa1Z!\x17-#\xb8\x02G\xf3\xb6\xca\x85\xff\xa9\x7f\x01\x8d\xb4\xdf\xcb'
                }
            },
            b"tentacoli-03-My Son's Friend Is A Champion Pisser.mp3": {
                '': {
                    'length': 2333412,
                    'pieces root': 
b';E\x8e\xc6{h\r\x05k\x9fH\xd2\x92\x02B\xbd\x81"I\xce\xbd\x199\x88\x03@\xe7\xd2\xe16\xfe\x0f'
                }
            },
            b'tentacoli-04-Summer And Winter.mp3': {
                '': {
                    'length': 2562622,
                    'pieces root': 
b"?\xf2\x0co\x03\x8c\x04\x8f'\xe4(I\x94\x98\xe1\xd1\x94\x88\xf8\xf94\xdff2\xd5\xd4\xd8'\xcc\xf3\xf3\xf2"
                }
            },
            b'tentacoli-05-San Diego, Yellow Cab.mp3': {
                '': {
                    'length': 2796486,
                    'pieces root': 
b'z\xd8^\x14\x00\xe6\x85\x97\xe0\x06\x04\rM\x93s\xd2\xa0]\x9b*\xe1?\x14\xdd\x9991}3\xea,\x8b'
                }
            },
            b'tentacoli-06-Happiness Is Having Two Killer Whales As Friends.mp3': {
                '': {
                    'length': 3863849,
                    'pieces root': 
b'\xd0\x06<-\x1f\x1c^;^\x163\x83\xd5\xf5\xa6:\xc9\x83\xbeYp\x8e\riXpD,\xeb\xbe7\xd9'
                }
            },
            b'tentacoli-07-Too Risky A Day For A Regatta.mp3': {
                '': {
                    'length': 3696387,
                    'pieces root': 
b"Gg\xdc\x7f\x1b\x8d/j\xaa\xe2v\x03L\xb2\xe2\xc6\xc6V\xafVh>\x14\x8f\x0eT\xc1S\x82'2E"
                }
            },
            b'tentacoli-08-Sorry, I Have To Go.mp3': {
                '': {
                    'length': 2583746,
                    'pieces root': 
b'f\x1b}\xc0\xbb\x90\x1d\x02cp\xbe\x0bs\xe8\x7f\xb6\xa9Ty\xdc\x85\xbc\xde\xfeaq\x1d\x8f\xc5\xb26\xf3'
                }
            },
            b'tentacoli-09-Scotch For Two.mp3': {
                '': {
                    'length': 1924024,
                    'pieces root': 
b'k\xdf\xbd\x14Zd\xca\xbf\xc9\xb0\x97\x1f\x87\xaei\xc9;\xd1\x84{\x0cH5\x1e\xd9q\xc1\xfaN\x9d\xb9\x7f'
                }
            },
            b"tentacoli-10-The Killer Whales' Games.mp3": {
                '': {
                    'length': 2463949,
                    'pieces root': 
b"\xe5'>:b\xde\xe1\xa1c(G\x1aG\x1f:6\x1e\xee\r\xe0}\xe6\x8a\x11\xb6i\\\x84\x83\xa4\xeb\x88"
                }
            },
            b'tentacoli-11-The Capture Of The Giant Octopus.mp3': {
                '': {
                    'length': 1783901,
                    'pieces root': 
b'\x91\xb3\xf6\xf3=|,\xc4u,;ny\xba\xfe~\xc2\xd3\xe0<1\xae7\x90\xa5\xc12\xd1\xa9\xb9?\x84'
                }
            },
            b'tentacoli-12-Two Old Kids.mp3': {
                '': {
                    'length': 2965602,
                    'pieces root': 
b'\x0b\t\xf3\x1aB\x06\xb8\xc4%0\xdc\x7f\xb4\x89\xeaR\xf4\xdbi\x92\xb1\xde\x8c\x9eV\xdd\x1b\xf7\x11\xd6J\xf4'
                }
            },
            b'tentacoli-13-Tentacles.mp3': {
                '': {
                    'length': 4319196,
                    'pieces root': 
b'&\xc6tK\x0b2E\xed#i\x172\xc2/\x95\xa9\xbf\x07O2\xa4\x9b\xa5\xc9\xfe\xec\xe8\t4~\x88\x9e'
                }
            },
            b"tentacoli-14-My Son's Friend Is A Champion Pisser (Versione 2).mp3": {
                '': {
                    'length': 1555839,
                    'pieces root': 
b'sd\x90\xac\x9aW%\x1d_\xeb\x02\xbb\x8e\x17\xd0\xf2\x9ev\x19\x93\xbd`N\xbe\xce/f\xe8\xcf\xc2Jv'
                }
            },
            b'tentacoli-15-Tentacles (Versione 2).mp3': {
                '': {
                    'length': 5261174,
                    'pieces root': 
b'L\x94\xd4\xd2\xd2\x061\xaf\xa6\xfaMB\x87\xafL\xf5\xc2b\xc0\x0f\x83L\x02|\x9d\xf3\xf7\x11\xad\xed\xb2\xdb'
                }
            },
            b'tentacoli-16-San Diego, Yellow Cab (Versione 2).mp3': {
                '': {
                    'length': 2877921,
                    'pieces root': 
b'\xb9\xf5\x0e\x8b\xaa\xc6\xb3\x8d\xc2\xf8N\x938{\x8c^!\xc34\xf5O\x87r\xcd]\xc8\x19\xe8\x8cg\x1ck'
                }
            },
            b"tentacoli-17-My Son's Friend Is A Champion Pisser (Versione 3).mp3": {
                '': {
                    'length': 1079679,
                    'pieces root': 
b'\xa2x\x1d\x81\xc2\xdc3\xda\xc1y\xbc\xe1C\x90H^G\xf2\x12\x9b\xa9\x8e\xd4\xef\xbdF\x90\xda\xf7"\x9d\x95'
                }
            },
            b'tentacoli-18-Too Risky A Day For A Regatta (Versione 2).mp3': {
                '': {
                    'length': 874993,
                    'pieces root': b'\xc6}\xe6k\xc0\xc2\xc4\x1f\xceA\x1d5\x1d\x82\xdf\xf8\xaf\xe8\t\xe5 
m\xf6\xb1MSmO\xf4\xc0\r\xbe'
                }
            },
            b'tentacoli-19-Tentacles (Versione 3).mp3': {
                '': {
                    'length': 3951350,
                    'pieces root': 
b'\xb6\x14Rc\xd7\x0ci\xc60\xd5\x9d\xfa\x07\x85fc\xb3>V!\xa6*3\xd7\xcdpl:\xaf\xdf\xe2\xed'
                }
            },
            b'tentacoli-20-Sails.mp3': {
                '': {
                    'length': 4358356,
                    'pieces root': 
b'rCmj\x06\x86\xa9\xa8=x$E"@6\xc7\x1d\xdea\xe1\x82N\xf5\xea#\x04b\xad\x95\xda\xa1\x1d'
                }
            },
            b'tentacoli-21-Sails (Versione 2).mp3': {
                '': {
                    'length': 2870766,
                    'pieces root': 
b"\xb2}\xf2s!s\xf9O\xb7?\xec\xe8\xd7\xca(d\xd2\xc0'\xd5\x18o\x07\x8d0\xc0\x1eC\x10\xd7\x00\xe0"
                }
            }
        }
    ),
    piece_layers={
        b'\x0b\t\xf3\x1aB\x06\xb8\xc4%0\xdc\x7f\xb4\x89\xeaR\xf4\xdbi\x92\xb1\xde\x8c\x9eV\xdd\x1b\xf7\x11\xd6J\xf4
': [
            b'\x8f\xac\xc9V\x1a\x85\x07\x03\x13\xa8\xd1e\x9b\t8\xad\xce\xd1e/\x13b\t\x93[\xa5Q\x13c\xf9b\xb1',
            b'\xea\x81\xa0\x87\rGP\x92W\xfd\x84.c\x16iN\xcdo\xf1s\xbc\x04\x95(\xc4\xe4F,\xa33fV'
        ],
        b'&\xc6tK\x0b2E\xed#i\x172\xc2/\x95\xa9\xbf\x07O2\xa4\x9b\xa5\xc9\xfe\xec\xe8\t4~\x88\x9e': [
            b'\x8c\x8e\xc0\x16\xa9x\xdbZ6}\x02\x0eT\x84y\xbe\xb5\t\xf7\xe4X=@\x1b\xff\xdf\x0f<\x00\xfeUm',
            b'>\xa5R\xa6\x85$\xe4\x99\x02\xedQ9\x86\xd3\xf4 A\x80\xcf\x13\xe5p\xd4\x8e\x85\x86z\x10\xf39\xc0r',
            b'\xe2Q\xff\xa4\xd5#\xd2rg\xf9\xb6\x99)[\xca\xd2\xd0\xa9\x96\xfa\x0coQ7U\xe0\x92)\x8e\xd6\x1b\xca'
        ],
        b';E\x8e\xc6{h\r\x05k\x9fH\xd2\x92\x02B\xbd\x81"I\xce\xbd\x199\x88\x03@\xe7\xd2\xe16\xfe\x0f': [
            b'VI|Irt\x93\x85\xc2\xd7\xc7A\x82\xbb\x96\xa2\x1a\xba#\x9b\xdfEl\x9a\x01\xa4\x7f\xa8\x87^S\xd5',
            b'b\x19\x1c7)\x10\xff\x8a\x14\xb1{\x10\xbe%\xb4\xeb\x88_\xc3\x01\xaf\x90[JO\x14\xba\xcc\\kx\xf5'
        ],
        b"?\xf2\x0co\x03\x8c\x04\x8f'\xe4(I\x94\x98\xe1\xd1\x94\x88\xf8\xf94\xdff2\xd5\xd4\xd8'\xcc\xf3\xf3\xf2": [
            b'\xd7\x83\xb5\xe8\n\x04\xef\x8c\x8b\xa2~\xb9\xb4m\xdb6\xdb\xf8N\xb2=\xca\xf2\x17\x01\xa7\xb8\x80\xc4\x
12\x0fO',
            b'\x94k\x13\x13\x81\xb6\xda#f\x04\xb9\xe8`n\xed\xd43GG)h.\xfag\xaej\xfagY\xe4\x97\xaa'
        ],
        b"Gg\xdc\x7f\x1b\x8d/j\xaa\xe2v\x03L\xb2\xe2\xc6\xc6V\xafVh>\x14\x8f\x0eT\xc1S\x82'2E": [
            b'\xc8\xe3\xd4$\xf84\x13\x02\xfc\x17>\\\xa8\x12\xbd\x0e\xf5\xc4\xe6F\x98^\x08?\xaa\x91O\xa1\x95\xad#\x8
8',
            b'\xbc\xf0\x82j9\x9a\xccC\x9c\xf9\xc8m\x13K\xdc\xc8\xe3\xef\xfb\x08\x0c\x08\x9f\x94\xd8{\xd6\xc8\xdb\x8
2\x8c\xae'
        ],
        b'L\x94\xd4\xd2\xd2\x061\xaf\xa6\xfaMB\x87\xafL\xf5\xc2b\xc0\x0f\x83L\x02|\x9d\xf3\xf7\x11\xad\xed\xb2\xdb'
: [
            b'\x84\x17\x02\x1a\xaf\xe4lA/\x8d>F\xebQ\x8e\xf4\x8d1\x8b\xb5\x93\xebu\xa8\x87\xbeH\x8f\x96\x15\xf2\xd7
',
            b'\xb8\xc34\x12\xfd\x8f*\x91\xdcFg\x01e\xea\x8bX~Lo\xfd\x1f\xea\xe0\xce\xaa\xcc\x1c\x942\x13E<',
            b'E\x01\x85\xc4\x88\x80\xe05<\xd9\xea[M?:\x87\xc80\x0b\xa4\x9945\xca\xeb\xb1D\x03d7\xa7\xff'
        ],
        b'f\x1b}\xc0\xbb\x90\x1d\x02cp\xbe\x0bs\xe8\x7f\xb6\xa9Ty\xdc\x85\xbc\xde\xfeaq\x1d\x8f\xc5\xb26\xf3': [
            b'\x12\x01sx}\x8b6\x99&\x98\x1b\xfa\xd1\xde\x82%\x18h\xa1\xb3\x98\x92\xb7r\x7f[4\x87\xb6Yk\xf7',
            b'\xfc\x1cV\xbb\xf6\xed\x9e\xef\x90H\xe8\x1f\xeb\x87\xd1\xe0\xcc\xd0\xf0\x16\x7f\xeb}\xea\xec\x0cU\x913
c\xc8\xce'
        ],
        b'rCmj\x06\x86\xa9\xa8=x$E"@6\xc7\x1d\xdea\xe1\x82N\xf5\xea#\x04b\xad\x95\xda\xa1\x1d': [
            b'\x8alDf\x8e~\x9c\xcd\x81\x95\xbe\x8dG\xdds8u\x96$\xf8\xe1[\xf0\xc0\xfc\xb0\xa9\xbbEAcn',
            b'\xd6G\x89)&9\x91\xe0/\xad\x19\x1dM&\x10a\xb4j;)?\x13\xf31jp\xb4\x07/N\xe2\xdb',
            b'\xc3V~p\x1d\xce\xd6\xd1fz{\xc3@a\xeeWj\xa3\xf0P\xc0\xff\x07\x87"B\xcd\x1a\xd8\xba\xd9]'
        ],
        b'z\xd8^\x14\x00\xe6\x85\x97\xe0\x06\x04\rM\x93s\xd2\xa0]\x9b*\xe1?\x14\xdd\x9991}3\xea,\x8b': [
            b'jJ2%\xceQ\x94>m]\xd7\x93\x06\xcc\x00y\x90\xdco\xd0\x13\x0f\xc7v\xbc\xafl\x03\xc1\xf0Jh',
            b"K'\x81\xc6{\xc9\xd9G\xdc(\xd9\xed\xc1y\xef\xb4\xfe\x0e\xcd?{\xa2\x87Ms\x829Y\xdc\xe0\xd9\xff"
        ],
        b"\xb2}\xf2s!s\xf9O\xb7?\xec\xe8\xd7\xca(d\xd2\xc0'\xd5\x18o\x07\x8d0\xc0\x1eC\x10\xd7\x00\xe0": [
            b'@\xcd\xec\xdf\xcfe\x96,R\x86C\x99\x97\xd3\xac\xbd-\xb5 
\x80\xd4`\xab\x02\xbc\x86\xeb\xe4A\x98\x0f\xc5',
            b"\x81H\xfe\xd1V'\x1a\xf32\xff\x96J\xf1\t\xf6=lS\xdb\xc7\x89\x1c\xfdYq1.Z&\xec\xad\xc1"
        ],
        b'\xb6\x14Rc\xd7\x0ci\xc60\xd5\x9d\xfa\x07\x85fc\xb3>V!\xa6*3\xd7\xcdpl:\xaf\xdf\xe2\xed': [
            b']\xb7\x90\xbc\xae\x080+$\x05R\x0f\x16V\xf4\xd8YG\x94\x05\xa3lb+\xbcL\xdf\xae\x17\xb8\xf7\x00',
            b'.\xe8\x93\xe4\xf5B\xf2\xbe\x97K;I\x0f\x81\xa0\x16\x9b\xe0\xda\x87\xbfcKq\x05%t\xb9\x1d\x89\x1e9'
        ],
        b'\xb9\xf5\x0e\x8b\xaa\xc6\xb3\x8d\xc2\xf8N\x938{\x8c^!\xc34\xf5O\x87r\xcd]\xc8\x19\xe8\x8cg\x1ck': [
            b'\x0e\xbf\xc8\x9aM\xa5\xb3\x1f\xd7?\x02EV\x92\xb9Ry\xb9w\xc0yN\x1f\xe6\xb5\xd8\xb5w\x97\x84R\x0e',
            b'\x99[\x89~\xbe\xd7\x1e\xc7_a\x06\x98\xe4cz\x0bv\xdf\xea\\8\xd2\x039\xe8\x1d\xbc-\xf8\xcam\x82'
        ],
        b'\xd0\x06<-\x1f\x1c^;^\x163\x83\xd5\xf5\xa6:\xc9\x83\xbeYp\x8e\riXpD,\xeb\xbe7\xd9': [
            b'X\x1c\x13\xfd\xa7\x98\xec,\x82|w!\xb8U\xf4\x83\x00R\x1f\\\xe1\xe3\x10Q\xc7\xab\x82\xe32\xa0\xb3D',
            b"u\xeaOin[\x92j\xf8\r\xa9ms'\x98\xa7\xca\xe0\xea5\x8b\xf9\xa8\x9aIr22\xec\x14\x98g"
        ],
        b'\xd1\xc3\xd8\x0b\xf1?\xd4+>\x85\x82\xa1Z!\x17-#\xb8\x02G\xf3\xb6\xca\x85\xff\xa9\x7f\x01\x8d\xb4\xdf\xcb'
: [
            b's\xa4\xa9D\x96;\x88)g\xc6vV=\xb7)\x06\x16\x96>\x8e\xa0$\xc3\x17\xbdf\xe8\x99\x87\xcb[\xf7',
            b'\x97\\\x80\xb2\x9c\xa0:\xfb\x12\xfb\x0e\xb7k\x1b\xce\xa5>@\xfd\xd4u\x9a\xf9M"%OR\x10\xb9M\x06'
        ],
        b"\xe5'>:b\xde\xe1\xa1c(G\x1aG\x1f:6\x1e\xee\r\xe0}\xe6\x8a\x11\xb6i\\\x84\x83\xa4\xeb\x88": [
            b"\xa2\xa7V\x9c\x9d\xdf{\xaf\xe4fM\xec&\x00\xdc\x86\xcb\xcc^\xcf[)\x16&\xfd'z\x1c\x17\t\x15\x85",
            b"\x0f\x9f\xcc\xce\xe3\x19\x1f\x98jC%\xbd'\xbc\x8e\x1b\xd3\xb1X,7\xb8~\x83h\x1d<\xf8\xc9^\xf7\xe6"
        ],
        b'\xe7Up\x0c[\xce\xa4\x90Z\x1a?\x90\x03Q\xd8\xa5V@\x98\xbc\x08\x1egT\xe3y\xf8\x05r\x8d\x19\n': [
            b"\xbdg%\xa2\x00\xae\x0f(\x83\xad\x1f\x8e5\xa5Q]\xf7'``u\x83\xfb\x12eS\x8fY/\xda\x84\xb1",
            b"A'4\x92\xc6'\xecJE<}~]|\x8eD)\xe2`;\x06d\xc2\xec8\xd8\xee\xa0\xb6\x1d\xd7\x9d"
        ]
    },
    url_list=['https://example.com/files']
)

It handles conversion to and from bytes and strings, so within python it works as expected and you don’t need to worry about serialization.

torrent.announce_list = [[torrent.announce], ["https://example.com/announce"]]
torrent.comment = "you better believe it's great, seriously check it out"
torrent.created_by = "not me, but i like them whoever they are"

torrent.write('new-tentacoli.torrent')
edited = Torrent.read('new-tentacoli.torrent')
edited.pprint()
                      tentacles-tentacoli-1977-ost-soundtrack                      
┌──────────────┬──────────────────────────────────────────────────────────────────┐
│ # Files      │ 21                                                               │
│ Total Size   │ 57.3 MiB                                                         │
│ Piece Size   │ 2.0 MiB                                                          │
│ Torrent Size │ 7.9 KiB                                                          │
│ V1 Infohash  │ 15590c30a2ffe45e7b2cf17c90256adec1d638dc                         │
│ V2 Infohash  │ 1bebedb6d2f396ba4e22fc2cc317987be244062be177419d5094716de8422194 │
└──────────────┴──────────────────────────────────────────────────────────────────┘
{
    'announce': 'udp://example.com:6969',
    'announce-list': [['udp://example.com:6969'], ['https://example.com/announce']],
    'comment': "you better believe it's great, seriously check it out",
    'created by': 'not me, but i like them whoever they are',
    'creation date': 1748082221,
    'info': {
        'name': 'tentacles-tentacoli-1977-ost-soundtrack',
        'source': 'i forgot at this point',
        'piece length': 2097152,
        'meta version': 2
    },
    'url-list': 'https://example.com/files'
}

Create a torrent

Have files but no torrent? no problem. torrent-models has a special :class:~torrent_models.TorrentCreate class with convenience methods and fields for creating torrents

from pathlib import Path
import random

from torrent_models import TorrentCreate, MiB

folder = Path("my_files")
folder.mkdir(exist_ok=True)
for file in ('secrets.bin', 'plots.bin'):
    with open(folder / file, 'wb') as f:
        f.write(random.randbytes(random.randint(10*(2**10), 10*(2**20))))

new = TorrentCreate(path_root=folder, piece_length=1 * MiB, announce="udp://example.com:6969")
v1 = new.generate("v1")
v2 = new.generate("v2") 
hybrid = new.generate("hybrid")    
v1.pprint(verbose=3)

Hide code cell output

                         my_files                          
┌──────────────┬──────────────────────────────────────────┐
│ # Files      │ 2                                        │
│ Total Size   │ 7.9 MiB                                  │
│ Piece Size   │ 1.0 MiB                                  │
│ Torrent Size │ 398 Bytes                                │
│ V1 Infohash  │ 094699828ca2631b16754e1eeaf9914f6bcc81d4 │
└──────────────┴──────────────────────────────────────────┘
{
    'announce': 'udp://example.com:6969',
    'created by': 'torrent-models (0.3.4.dev10+gf0da03c)',
    'info': {
        'name': 'my_files',
        'pieces': [
            'a896c1024dbecd98f9a2a6d3c133e6e163ff64a6',
            '3f22cfa8f3876cd3c97745b73827252688b74756',
            '40679f1686ba693dc865aedb6177b7c0363c757e',
            '0f61697b92754ccafa3ec90c35d401d292f6ab51',
            'aad1972356863b550ed88399bbceb74c4bc6bb04',
            '0f169ad5d6fb8fc000b85bf4215c8633cfb0e390',
            '2314dbb76a2d70ffc471c7fe94208491c5b9df45',
            '5e12a979cd4985fc3fb733d2179c46ceb7ed7848'
        ],
        'files': [{'length': 6639879, 'path': ['plots.bin']}, {'length': 1683084, 'path': ['secrets.bin']}],
        'piece length': 1048576
    }
}
v2.pprint(verbose=3)

Hide code cell output

                                     my_files                                      
┌──────────────┬──────────────────────────────────────────────────────────────────┐
│ # Files      │ 2                                                                │
│ Total Size   │ 7.9 MiB                                                          │
│ Piece Size   │ 1.0 MiB                                                          │
│ Torrent Size │ 720 Bytes                                                        │
│ V2 Infohash  │ 3745e5c81dd5fe4a058ffe761c30e4284efb6b58259502be0a9e52efa608ba71 │
└──────────────┴──────────────────────────────────────────────────────────────────┘
{
    'announce': 'udp://example.com:6969',
    'created by': 'torrent-models (0.3.4.dev10+gf0da03c)',
    'info': {
        'name': 'my_files',
        'meta version': 2,
        'file tree': {
            b'plots.bin': {
                '': {
                    'length': 6639879,
                    'pieces root': '1f8688ecc2b927c24bb1627227c9bf6a14d533a22d7e56e49e5d036e747906ea'
                }
            },
            b'secrets.bin': {
                '': {
                    'length': 1683084,
                    'pieces root': '259d88e3681f115f93750ff2f3c1b7bc2c01279769706adb06dbdc294d74dc26'
                }
            }
        },
        'piece length': 1048576
    },
    'piece layers': {
        '1f8688ecc2b927c24bb1627227c9bf6a14d533a22d7e56e49e5d036e747906ea': [
            'a781677b8e26a0bd5f0a8eef9f476844ba27f309edffdfdcfff63ae94f929533',
            'c171258eb5bcf68c0c248ad483e8acaf6f4f07be0c847c0c3ea361f6c3e0785a',
            'bfebcdb0f0220fae3f4c9360a320e0ea78a93ad780cf493ab619a129d2e8ff4f',
            '23990c8504058e7e1cdc1299284d3c98d5f1f8264c8d4a4e42f20117d19b53ce',
            '12df7eaad6aa3c4ab508388127220f3b84e2bc767866d979fb525a05bd54a288',
            '760a8b8b52191c0ae2c41272df94421447708c919df41619dd2255ace07310c8',
            '6aeb010e00dc7c764f6c9868e2a56a1c67ff387a0b8a562cf462a64dea0400d2'
        ],
        '259d88e3681f115f93750ff2f3c1b7bc2c01279769706adb06dbdc294d74dc26': [
            'c0e536668a700219cbbff87769fbd42002c8f098581a4916456ebe8dfaecc403',
            '89fcb1bed7c100b2d25cbcbff001d9ec538813a408981a52f8c6dc93bf60f89a'
        ]
    }
}
hybrid.pprint(verbose=3) 

Hide code cell output

                                     my_files                                      
┌──────────────┬──────────────────────────────────────────────────────────────────┐
│ # Files      │ 2                                                                │
│ Total Size   │ 7.9 MiB                                                          │
│ Piece Size   │ 1.0 MiB                                                          │
│ Torrent Size │ 1.1 KiB                                                          │
│ V1 Infohash  │ 780774c30c8d89db1db0af53ea9ce13e32948576                         │
│ V2 Infohash  │ 06134413e340c1eb75fe97dc6b044a5b47173e1ef1c6c92dbc005098dbb934fb │
└──────────────┴──────────────────────────────────────────────────────────────────┘
{
    'announce': 'udp://example.com:6969',
    'created by': 'torrent-models (0.3.4.dev10+gf0da03c)',
    'info': {
        'name': 'my_files',
        'pieces': [
            'a896c1024dbecd98f9a2a6d3c133e6e163ff64a6',
            '3f22cfa8f3876cd3c97745b73827252688b74756',
            '40679f1686ba693dc865aedb6177b7c0363c757e',
            '0f61697b92754ccafa3ec90c35d401d292f6ab51',
            'aad1972356863b550ed88399bbceb74c4bc6bb04',
            '0f169ad5d6fb8fc000b85bf4215c8633cfb0e390',
            'ac2a096ad7fb9dbe5a46a5c2983043388492cac9',
            'bc16ad511d5c790ab2ca2e45b8a25ffe6d65f6bc',
            '69f3ef898d53d99b020a57ab265a3463ee9e7d9b'
        ],
        'files': [
            {'length': 6639879, 'path': ['plots.bin']},
            {'length': 700153, 'path': ['.pad', '700153'], 'attr': b'p'},
            {'length': 1683084, 'path': ['secrets.bin']},
            {'length': 414068, 'path': ['.pad', '414068'], 'attr': b'p'}
        ],
        'piece length': 1048576,
        'meta version': 2,
        'file tree': {
            b'plots.bin': {
                '': {
                    'length': 6639879,
                    'pieces root': '1f8688ecc2b927c24bb1627227c9bf6a14d533a22d7e56e49e5d036e747906ea'
                }
            },
            b'secrets.bin': {
                '': {
                    'length': 1683084,
                    'pieces root': '259d88e3681f115f93750ff2f3c1b7bc2c01279769706adb06dbdc294d74dc26'
                }
            }
        }
    },
    'piece layers': {
        '1f8688ecc2b927c24bb1627227c9bf6a14d533a22d7e56e49e5d036e747906ea': [
            'a781677b8e26a0bd5f0a8eef9f476844ba27f309edffdfdcfff63ae94f929533',
            'c171258eb5bcf68c0c248ad483e8acaf6f4f07be0c847c0c3ea361f6c3e0785a',
            'bfebcdb0f0220fae3f4c9360a320e0ea78a93ad780cf493ab619a129d2e8ff4f',
            '23990c8504058e7e1cdc1299284d3c98d5f1f8264c8d4a4e42f20117d19b53ce',
            '12df7eaad6aa3c4ab508388127220f3b84e2bc767866d979fb525a05bd54a288',
            '760a8b8b52191c0ae2c41272df94421447708c919df41619dd2255ace07310c8',
            '6aeb010e00dc7c764f6c9868e2a56a1c67ff387a0b8a562cf462a64dea0400d2'
        ],
        '259d88e3681f115f93750ff2f3c1b7bc2c01279769706adb06dbdc294d74dc26': [
            'c0e536668a700219cbbff87769fbd42002c8f098581a4916456ebe8dfaecc403',
            '89fcb1bed7c100b2d25cbcbff001d9ec538813a408981a52f8c6dc93bf60f89a'
        ]
    }
}

Installation

From pypi

python -m pip install torrent-models

From git

git clone https://github.com/p2p-ld/torrent-models
cd torrent-models
python -m venv .venv
source .venv/bin/activate
python -m pip install -e .

Other Projects

These are also good projects, and probably more battle tested (but we don’t know them well and can’t vouch for their use):

The reasons we did not use these other tools and wrote this one:

  • torf has some notable performance problems, and doesn’t support v2. In several issues, the maintainer has signaled that the package needs to be rewritten, and will not support v2 until then.

  • torrentfile is focused on the cli and doesn’t appear to be able to validate torrent files, and there is no dedicated method for parsing them, e.g. editing directly manipulates the bencoded dict and rebuilding requires the files to be present

  • dottorrent can only write, not parse torrent files.

  • torrenttool doesn’t validate torrents

  • PyBitTorrent doesn’t validate torrents

  • torrent_parser doesn’t validate torrents and doesn’t have a torrent file class