Skip to content

Instantly share code, notes, and snippets.

@rmmh
Last active March 19, 2018 21:16
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rmmh/49d5f426351784d8980ac240e8f50c40 to your computer and use it in GitHub Desktop.
Save rmmh/49d5f426351784d8980ac240e8f50c40 to your computer and use it in GitHub Desktop.
Tiny .torrent file creator.
#!/usr/bin/env python2
'''
A tiny .torrent file creator with no external dependencies.
Easy to hack!
'''
import argparse
import hashlib
import os
from cStringIO import StringIO
def bencode(v):
# https://en.wikipedia.org/wiki/Bencode
buf = StringIO()
def emit(v):
if isinstance(v, (int, long, bool)):
buf.write('i%de' % v)
elif isinstance(v, str):
buf.write('%d:%s' % (len(v), v))
elif isinstance(v, dict):
buf.write('d')
for k, v in sorted(v.items()):
emit(k)
emit(v)
buf.write('e')
elif isinstance(v, list):
buf.write('l')
for e in v:
emit(e)
buf.write('e')
else:
raise TypeError('unhandled type: %s in %r' % (type(v), v))
emit(v)
return buf.getvalue()
class Torrent(object):
def __init__(self, base_dir=None, piece_length=2**(8*10)):
self.base_dir = base_dir
self.piece_length = piece_length
self.pieces = ''
self.hasher = hashlib.sha1()
self.hasher_fill = 0
self.files = []
self.file_set = set()
def add_file(self, path, torrent_path=None):
if torrent_path is None:
torrent_path = path
if self.base_dir:
torrent_path = os.path.relpath(path, self.base_dir)
assert torrent_path not in self.file_set, 'Duplicate file: %s' % torrent_path
self.file_set.add(torrent_path)
self.files.append({'path': torrent_path.split('/'), 'length': os.stat(path).st_size})
with open(path, 'rb') as f:
while True:
chunk = f.read(min(self.piece_length - self.hasher_fill, self.piece_length))
if not chunk:
break
self.hasher.update(chunk)
self.hasher_fill += len(chunk)
assert self.hasher_fill <= self.piece_length
if self.hasher_fill == self.piece_length:
self.pieces += self.hasher.digest()
self.hasher = hashlib.sha1()
self.hasher_fill = 0
def get_encoded(self, name, **kwargs):
if self.hasher_fill:
self.pieces += self.hasher.digest()
self.hasher = None
d = {
'info': {
'name': name,
'piece length': self.piece_length,
'pieces': self.pieces,
}
}
if len(self.files) > 1:
d['info']['files'] = self.files
elif self.files:
d['info']['length'] = self.files[0]['length']
else:
raise ValueError('cannot create empty torrent file')
d.update((k,v) for k,v in kwargs.iteritems() if v)
return bencode(d)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-a', '--announce', help='Tracker announce URL.')
parser.add_argument('-p', '--private', action='store_true', help='Disable peer exchange and DHT.')
parser.add_argument('-s', '--piece_length', type=int, default=18, choices=range(10, 31),
help='Power of 2 chunk size (default: 18=256KiB).')
parser.add_argument('-b', '--base', help='Base directory to make paths relative to.')
parser.add_argument('-n', '--name', help='Suggested name of the torrent. Defaults to base directory name or first file directory name.')
parser.add_argument('-o', '--output', metavar='OUT.TORRENT', help='Output .torrent name.')
parser.add_argument('files', nargs='+', help='Files to include in torrent.')
options = parser.parse_args()
piece_length = 2 ** options.piece_length
if piece_length < 2**20:
print 'Using %d KiB chunks.' % (piece_length / 1024)
else:
print 'Using %d MiB chunks.' % (piece_length / 1024 ** 2)
torr = Torrent(base_dir=options.base, piece_length=piece_length)
for fname in options.files:
assert os.path.isfile(fname)
torr.add_file(fname)
print 'Added', fname
name = options.files[0]
if options.name:
name = options.name
elif options.base:
name = os.path.basename(options.base)
elif len(options.files) > 1:
name = os.path.basename(os.path.dirname(os.path.abspath(options.files[0])))
encoded = torr.get_encoded(name=name, private=options.private, announce=options.announce)
with open(options.output, 'w') as f:
f.write(encoded)
print 'Done.'
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment