├── .gitignore ├── makeTorrent ├── __init__.py └── makeTorrent.py ├── setup.py ├── LICENSE ├── README.md └── README.rst /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist/ 3 | .DS_STORE 4 | build/ 5 | -------------------------------------------------------------------------------- /makeTorrent/__init__.py: -------------------------------------------------------------------------------- 1 | from makeTorrent import makeTorrent 2 | 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='makeTorrent', 5 | version='0.14', 6 | packages=['makeTorrent'], 7 | install_requires=[ 8 | 'bencode', 9 | ], 10 | license='MIT', 11 | description='Basic library for creating torrents', 12 | long_description=open('README.rst').read(), 13 | author='Stewart Rutledge', 14 | author_email='stew.rutledge@gmail.com', 15 | url='https://github.com/stewrutledge/makeTorrent' 16 | ) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Stewart Rutledge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | makeTorrent 2 | ----------- 3 | 4 | 5 | A basic python library for generating single and multi-file torrents. 6 | 7 | 8 | Usage 9 | ----- 10 | 11 | ```python 12 | from makeTorrent import makeTorrent 13 | 14 | mk = makeTorrent(announce='http://example.com/announce') 15 | ``` 16 | 17 | At this point the makeTorrent class contains a dictionary, which can be printed either as a dict or as a bencoded string (torrent format). 18 | 19 | To add files, use either the multi_file class or the single_file class 20 | 21 | ```python 22 | mk.multi_file('/path/to/directory') 23 | 24 | with open('my.torrent') as tf: 25 | tf.write(mk.getBencoded()) 26 | ``` 27 | 28 | The same method can be used with `mk.single_file` with just pointing out a single file. 29 | 30 | Notes 31 | ----- 32 | 33 | There are a number of items that can be added when initializing the class: 34 | 35 | ```python 36 | mk = makeTorrent( 37 | announce='http://example.com/announce', 38 | comment='Test Torrent', 39 | httpseeds=['http://example.com/file.iso'], 40 | announcelist=[['http://announce1.example.com'],['http://announce2.example.com']] 41 | ) 42 | ``` 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | makeTorrent 3 | =========== 4 | 5 | 6 | A basic python library for generating single and multi-file torrents. 7 | 8 | 9 | Usage 10 | ===== 11 | 12 | Basic usage: 13 | 14 | .. code-block:: python 15 | 16 | from makeTorrent import makeTorrent 17 | 18 | mk = makeTorrent(announce='http://example.com/announce') 19 | 20 | 21 | At this point the makeTorrent class contains a dictionary, which can be printed either as a dict or as a bencoded string (torrent format). 22 | 23 | To add files, use either the multi_file class or the single_file class: 24 | 25 | .. code-block:: python 26 | 27 | mk.multi_file('/path/to/directory') 28 | 29 | with open('my.torrent') as tf: 30 | tf.write(mk.getBencoded()) 31 | 32 | The same method can be used with `mk.single_file` with just pointing out a single file. 33 | 34 | Notes 35 | ===== 36 | 37 | There are a number of items that can be added when initializing the class: 38 | 39 | .. code-block:: python 40 | 41 | mk = makeTorrent( 42 | announce='http://example.com/announce', 43 | comment='Test Torrent', 44 | httpseeds=['http://example.com/file.iso'], 45 | announcelist=[['http://announce1.example.com'],['http://announce2.example.com']] 46 | ) 47 | 48 | 49 | -------------------------------------------------------------------------------- /makeTorrent/makeTorrent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from bencode import bencode 4 | from hashlib import md5, sha1 5 | from os import path, walk 6 | from time import time 7 | from urllib2 import urlparse 8 | 9 | 10 | class makeTorrent: 11 | """ 12 | Create single or multi-file torrents 13 | keywords: 14 | announcelist: a list of lists with announceurls [['http://example.com/announce']] 15 | httpseeds: a list of urls ['http://1.com/file', http://2.com/file] 16 | comment: text comment for torrent 17 | Several values are generated automtically: 18 | name: based on filename or path 19 | techincal info: generates when a sinlge or multi file is added 20 | """ 21 | def __init__(self, announce, piece_length=262144, **kw): 22 | self.piece_length = piece_length 23 | if not bool(urlparse.urlparse(announce).scheme): 24 | raise ValueError('No schema present for url') 25 | self.tdict = { 26 | 'announce': announce, 27 | 'creation date': int(time()), 28 | 'info': { 29 | 'piece length': self.piece_length 30 | } 31 | } 32 | if kw.get('comment'): 33 | self.tdict.update({'comment': kw.get('comment')}) 34 | if kw.get('httpseeds'): 35 | if not isinstance(kw.get('httpseeds'), list): 36 | raise TypeError('httpseeds must be a list') 37 | else: 38 | self.tdict.update({'httpseeds': kw.get('httpseeds')}) 39 | if kw.get('announcelist'): 40 | if not isinstance(kw.get('announcelist'), list): 41 | raise TypeError('announcelist must be a list of lists') 42 | if False in [isinstance(l, list) for l in kw.get('announcelist')]: 43 | raise TypeError('announcelist must be a list of lists') 44 | if False in [bool(urlparse.urlparse(f[0]).scheme) for f in kw.get('announcelist')]: 45 | raise ValueError('No schema present for url') 46 | else: 47 | self.tdict.update({'announce-list': kw.get('announcelist')}) 48 | 49 | def getDict(self): 50 | return(self.tdict) 51 | 52 | def info_hash(self): 53 | return(sha1(bencode(self.tdict['info'])).hexdigest()) 54 | 55 | def getBencoded(self): 56 | return(bencode(self.tdict)) 57 | 58 | def multi_file(self, basePath, check_md5=False): 59 | """ 60 | Generate multi-file torrent 61 | check_md5: adds md5sum to the torrentlist 62 | basePath: path to folder 63 | Torrent name will automatically be basePath 64 | """ 65 | if 'length' in self.tdict['info']: 66 | raise TypeError('Cannot add multi-file to single-file torrent') 67 | if basePath.endswith('/'): 68 | basePath = basePath[:-1] 69 | realPath = path.abspath(basePath) 70 | toGet = [] 71 | fileList = [] 72 | info_pieces = '' 73 | data = '' 74 | for root, subdirs, files in walk(realPath): 75 | for f in files: 76 | subPath = path.relpath(path.join(root, f), start=realPath).split('/') 77 | subPath = [str(p) for p in subPath] 78 | toGet.append(subPath) 79 | for pathList in toGet: 80 | length = 0 81 | filePath = ('/').join(pathList) 82 | if check_md5: 83 | md5sum = md5() 84 | fileDict = { 85 | 'path': pathList, 86 | 'length': len(open(path.join(basePath, filePath), "rb").read()) 87 | } 88 | with open(path.join(basePath, filePath), "rb") as fn: 89 | while True: 90 | filedata = fn.read(self.piece_length) 91 | 92 | if len(filedata) == 0: 93 | break 94 | length += len(filedata) 95 | 96 | data += filedata 97 | 98 | if len(data) >= self.piece_length: 99 | info_pieces += sha1(data[:self.piece_length]).digest() 100 | data = data[self.piece_length:] 101 | 102 | if check_md5: 103 | md5sum.update(filedata) 104 | fileDict['md5sum'] = md5sum.hexdigest() 105 | fileList.append(fileDict) 106 | if len(data) > 0: 107 | info_pieces += sha1(data).digest() 108 | self.tdict['info'].update( 109 | { 110 | 'name': str(path.basename(realPath)), 111 | 'files': fileList, 112 | 'pieces': info_pieces 113 | } 114 | ) 115 | info_hash = sha1(bencode(self.tdict['info'])).hexdigest() 116 | return({'Created': info_hash}) 117 | #return(info_pieces, fileList) 118 | 119 | def single_file(self, fileName, check_md5=False): 120 | """ 121 | Creates a torrent containing one file 122 | fileName: file to create torrent from 123 | check_md5: add md5sum to torrent 124 | Torrent name will be filename 125 | """ 126 | if 'files' in self.tdict['info']: 127 | raise TypeError('Cannot add single file to multi-file torrent') 128 | info_pieces = '' 129 | data = '' 130 | realPath = path.abspath(fileName) 131 | length = 0 132 | if check_md5: 133 | md5sum = md5() 134 | with open(realPath, "rb") as fn: 135 | while True: 136 | filedata = fn.read(self.piece_length) 137 | 138 | if len(filedata) == 0: 139 | break 140 | 141 | length += len(filedata) 142 | 143 | data += filedata 144 | 145 | if len(data) >= self.piece_length: 146 | info_pieces += sha1(data[:self.piece_length]).digest() 147 | data = data[self.piece_length:] 148 | 149 | if check_md5: 150 | md5sum.update(filedata) 151 | if len(data) > 0: 152 | info_pieces += sha1(data).digest() 153 | 154 | self.tdict['info'].update( 155 | { 156 | 'length': length, 157 | 'pieces': info_pieces, 158 | 'name': str(path.basename(realPath)) 159 | } 160 | ) 161 | if check_md5: 162 | self.tdict['info'].update( 163 | { 164 | 'md5sum': md5sum.hexdigest() 165 | } 166 | ) 167 | info_hash = sha1(bencode(self.tdict['info'])).hexdigest() 168 | return({'Created': info_hash}) 169 | --------------------------------------------------------------------------------