Two modules that run a BitTorrent server, and uses Twisted as a client to coordinate control-message passing, and progress monitoring. The server can be run as a separate process, or as a thread within the client -- the same messages can be passed back and forth.
Control messages can cancel individual downloads (or the whole process), as well as pause downloading. Progress queries can be invoked through the client, which will ping the server, and report back each downloads' progress.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 | ############
# BitTorrent server launch
############
import logging,sys,pdb,time,traceback,os
from thread import get_ident
from operator import mod
#need this prior to BT imports
import gettext
gettext.install('bittorrent', 'locale')
from BitTorrent.launchmanycore import LaunchMany
from BitTorrent.defaultargs import get_defaults
from BitTorrent.parseargs import parseargs, printHelp
from BitTorrent import configfile
from BitTorrent import BTFailure
from BitTorrent.bencode import bdecode
from twisted.spread import pb
from twisted.internet import reactor
from twisted.internet import threads
class DataLock:
def __init__(self):
if sys.platform == 'win32':
from qt import QMutex
self.mutex = QMutex(True)
elif sys.platform == 'darwin':
from Foundation import NSRecursiveLock
self.mutex = NSRecursiveLock.alloc().init()
def lock(self): self.mutex.lock()
def unlock(self): self.mutex.unlock()
def downloadDir():
ddir=None
if sys.platform=='win32':
ddir = os.environ.get('HOMEPATH')
if ddir and len(ddir)>0:
ddir = '%s%sMy Documents%sTorrentServer' % (ddir,os.sep,os.sep)
if not os.path.isdir(ddir): os.makedirs(ddir)
else: ddir='.'
else:
ddir = os.environ.get('HOME')
try:
ddir += '/Documents/TorrentServer'
if not os.path.isdir(ddir): os.makedirs(ddir)
except: ddir = os.environ.get('HOME')
return ddir
class TorrentServer(pb.Root):
torrentPort = 11989
log = None
tServer = None
isProcess = True
def __init__(self, td):
TorrentServer.tServer = self
self.launcher = None
self.torrentDir = td
self.torrentLock = DataLock()
self.status = ''
self.torrents = {}
self.stoppedTorrents = {}
self.moduloCount = 0
def listen(self):
from twisted.internet import reactor
TorrentServer.log.debug('init(%s): listening '%get_ident())
reactor.listenTCP( TorrentServer.torrentPort, pb.PBServerFactory(self))
reactor.run()
def remote_UpdateExecutionStatus(self, execStatus):
self.torrentLock.lock()
try:
TorrentServer.log.debug('remote_UpdateExecutionStatus(%s): %s' %\
(get_ident(),execStatus))
try:
td = self.torrentDir
ttdd = execStatus.get('torrent_dir',td)
self.status = execStatus.get('status','')
self.torrents = execStatus.get('torrents',{})
if td and ttdd!=td:
self.torrentDir = ttdd
self.status = 'restart'
elif not td: self.torrentDir = ttdd
except:
traceback.print_exc()
finally: self.torrentLock.unlock()
def remote_ReportProgressStatus(self):
#TorrentServer.log.debug('remote_ReportProgressStatus: ')
self.torrentLock.lock()
try: return {'torrents':self.torrents, 'status': self.status}
finally: self.torrentLock.unlock()
def initTorrent(self):
self.torrentLock.lock()
self.status=''
uiname = 'btlaunchmany'
defaults = get_defaults(uiname)
try:
config, args = configfile.parse_configuration_and_args(defaults, uiname, [], 0, 1)
#config, args = configfile.parse_configuration_and_args(defaults, uiname, sys.argv[1:], 0, 1)
config['torrent_dir'] = self.torrentDir
config['parse_dir_interval'] = 20 #make the dir scan happen @20 seconds, not default of 60
self.config = config
except BTFailure, e:
traceback.print_exc()
TorrentServer.log.error(_("%s\nrun with no args for parameter explanations")) % str(e)
self.torrentLock.unlock()
if self.torrentDir: self.runTorrents()
def runTorrents(self):
TorrentServer.log.debug('runTorrents(%s): LaunchMany... %s'%\
(get_ident(), self.torrentDir))
self.launcher = LaunchMany(self.config, self, 'btlaunchmany')
self.launcher.run()
TorrentServer.log.debug('runTorrents(%s): DONE with torrents...'%get_ident())
if self.status=='quit':
if TorrentServer.isProcess:
reactor.stop()
#sys.exit()
else:
if self.status=='restart':
log.debug('torrentServer(): Will restart %s '%self.torrentDir)
self.initTorrent()
if self.torrentDir: self.runTorrents()
def display(self, data):
self.torrentLock.lock()
try:
if self.status == 'quit': return True
if self.status=='restart': return True
while self.status=='paused':
#TorrentServer.log.debug( 'display(%s): is paused' % (get_ident()))
self.torrentLock.unlock()
time.sleep(1.0)
self.torrentLock.lock()
self.moduloCount += 1
modulo = mod(self.moduloCount, 3)
for xx in data:
( name, status, progress, peers, seeds, seedsmsg, dist,
uprate, dnrate, upamt, dnamt, size, t, msg ) = xx
if status is not 'downloading':
pass #TorrentServer.log.debug( 'display(%s): %s: %s (%s)' % (get_ident(),status, name, progress))
stopstatus = self.torrents.get(name)
if stopstatus and (stopstatus[0]=='cancel' or stopstatus[0]=='stop'):
try: os.remove(name)
except: traceback.print_exc()
del self.torrents[name]
else:
self.torrents[name] = ['progress',progress]
del data
return False
finally: self.torrentLock.unlock()
def message(self, str):
TorrentServer.log.debug('FeedbackReporter.message(): %s'%str)
def exception(self, str):
TorrentServer.log.warn('FeedbackReporter: exception=%s'%str)
def didEndTorrentThread(self,foo=None):
TorrentServer.log.debug('didEndTorrentThread(): %s'%foo)
def didEndTorrentThreadErr(self,foo=None):
TorrentServer.log.debug('didEndTorrentThreadErr(): %s'%foo)
def main(args):
if __debug__:
level = logging.DEBUG
else:
level = logging.WARN
logging.basicConfig(level=level, format='%(asctime)s %(levelname)s %(message)s')
handler = logging.StreamHandler()
TorrentServer.log = logging.getLogger('TorrentServer')
TorrentServer.log.addHandler(handler)
TorrentServer.log.propagate = 0
TorrentServer.log.debug('torrentServer.main(): will load config')
TorrentServer.tServer = TorrentServer(downloadDir())
dfr = threads.deferToThread(TorrentServer.tServer.initTorrent)
dfr.addCallback(TorrentServer.tServer.didEndTorrentThread)
dfr.addErrback(TorrentServer.tServer.didEndTorrentThreadErr)
TorrentServer.tServer.listen()
if __name__=="__main__":
main(sys.argv)
############
# Twisted-based client
############
import pdb,os,time,traceback
from twisted.internet import threads
from twisted.spread import pb
from twisted.internet import reactor
from torrentServer import *
class Torrenting(object):
def start(doLaunch=True):
theTorrenter = Torrenting(doLaunch)
start = staticmethod(start)
def __init__(self, doLaunch=True):
self.filenames = {}
self.didQuit = self.isPaused = False
self.torrents = {}
self.pinger = None
if doLaunch: self.launch()
def willQuit(self,event=None):
self.didQuit = True
if TorrentServer.tServer:
TorrentServer.tServer.didQuit = True
self.pinger.quitTorrents()
def _threadLaunch(self):
TorrentServer.tServer = TorrentServer(downloadDir())
TorrentServer.log = log
TorrentServer.tServer.initTorrent()
def didEndTorrentThread(self,foo=None):
pass
def didEndTorrentThreadErr(self,foo=None):
pass
def launch(self):
launched=False
if not __debug__:
if sys.platform=='win32':
TorrentServer.isProcess = True
dir,fn = os.path.split(sys.argv[0])
path = dir+os.sep+'torrentServer.exe'
try:
os.startfile(path)
launched=True
except: traceback.print_exc()
if not launched:
TorrentServer.isProcess = False
dfr = threads.deferToThread(self._threadLaunch)
dfr.addCallback(self.didEndTorrentThread)
dfr.addErrback(self.didEndTorrentThreadErr)
launched=True
if launched:
if TorrentServer.isProcess:
self.pinger = TorrentPingerRemote(self)
else: self.pinger = TorrentPingerLocal(self)
def gotProgress(self, progress):
torrents = progress.get('torrents',{})
for fn,status in torrents.iteritems():
if not self.torrents.has_key(fn): continue
if status[0]=='progress':
progressf = float(status[1])
if progressf==100.0: self.didDownloadTorrent(fn)
else: print 'gotProgress: %s, %.0f' % (fn, progressf)
elif status[0]=='failed':
self.didNotDownloadTorrent(fn)
def addTorrent(self,filename):
self.torrents[filename] = ['progress',0.0]
self.pinger.queryProgress()
def didCancelDownload(self,filename):
self.torrents[filename] = ['cancel']
try: del self.torrents[filename]
except: pass
self.didNotDownloadTorrent(filename)
def didDownloadTorrent(self,filename=None):
try: del self.filenames[filename]
except: pass
def didNotDownloadTorrent(self,filename=None):
try: del self.filenames[filename]
except: pass
def didCancelBuild(self):
for tt in self.torrents.keys():
try: os.remove(tt)
except: traceback.print_exc()
self.torrents = {}
self.pinger.cancelTorrents()
def togglePause(self):
self.isPaused = not self.isPaused
self.pinger.pauseTorrents(self.isPaused)
##########################
class TorrentPinger:
def __init__( self, delegate=None ):
self.delegate = delegate
self.progressPing = None
def _didFail( self, foo ):
try:
if self.progressPing:
self.progressPing.cancel()
self.progressPing = None
reactor.callLater(1,self.start)
except: traceback.print_exc()
def didPause(self,foo=None):
pass
def didQuit(self,foo=None):
pass
def didUpdateTorrents(self,foo=None):
pass
def updateTorrentsExecutionStatus(self, torrents):
cmds = {'torrents': torrents}
return cmds
def quitTorrents(self):
cmds = {'status':'quit'}
return cmds
def pauseTorrents(self, yn):
if yn: cmds = {'status':'paused'}
else: cmds = {'status':'active'}
return cmds
def queryProgress( self ):
pass
def _progressStatus( self, progress):
if self.delegate: self.delegate.gotProgress(progress)
self.progressPing = reactor.callLater(1, self.queryProgress )
def cancelTorrents(self):
try:
self.progressPing.cancel()
self.progressPing = None
except: traceback.print_exc()
class TorrentPingerLocal(TorrentPinger):
def __init__( self, delegate=None ):
TorrentPinger.__init__(self, delegate)
def _didFail(self,foo=None):
pass
def didPause(self,foo=None):
pass
def didQuit(self,foo=None):
pass
def didUpdateTorrents(self,foo=None):
pass
def updateTorrentsExecutionStatus(self, torrents):
cmds = TorrentPinger.updateTorrentsExecutionStatus(self,torrents)
def quitTorrents(self):
TorrentServer.tServer.remote_UpdateExecutionStatus(TorrentPinger.quitTorrents(self))
def pauseTorrents(self, yn):
dfr = reactor.callLater(0,TorrentServer.tServer.remote_UpdateExecutionStatus,
TorrentPinger.pauseTorrents(self,yn))
def queryProgress( self ):
status = TorrentServer.tServer.remote_ReportProgressStatus()
self._progressStatus(status)
class TorrentPingerRemote(TorrentPinger):
def __init__( self, delegate=None ):
TorrentPinger.__init__(self, delegate)
self.torrentServerRoot = None
reactor.callLater(1,self.start)
def start( self ):
try:
client = pb.PBClientFactory()
reactor.connectTCP('127.0.0.1', TorrentServer.torrentPort, client, 20)
dfr = client.getRootObject()
dfr.addCallbacks(self._gotRemote, self._didFail)
except: traceback.print_exc()
def _gotRemote( self, remote ):
try:
self.torrentServerRoot = remote
remote.notifyOnDisconnect(self._didFail)
except: traceback.print_exc()
def updateTorrentsExecutionStatus(self, torrents):
cmds = TorrentPinger.updateTorrentsExecutionStatus(torrents)
dfr = self.torrentServerRoot.callRemote('UpdateExecutionStatus', cmds)
dfr.addCallbacks(self.didUpdateTorrents, self._didFail)
def quitTorrents(self):
cmds = TorrentPinger.quitTorrents()
dfr = self.torrentServerRoot.callRemote('UpdateExecutionStatus', cmds)
dfr.addCallbacks(self.didQuit, self._didFail)
def pauseTorrents(self, yn):
cmds = TorrentPinger.pauseTorrents(yn)
dfr = self.torrentServerRoot.callRemote('UpdateExecutionStatus', cmds)
dfr.addCallbacks(self.didPause, self._didFail)
def queryProgress( self ):
dfr = self.torrentServerRoot.callRemote('ReportProgressStatus')
dfr.addCallbacks(self._progressStatus, self._didFail)
if __name__=="__main__":
png = TorrentPingerRemote()
reactor.callLater(3,png.queryProgress)
reactor.run()
|
These two modules will make integrating BitTorrent into your python app much easier. The server code is independent, and good to go. You'll want to replace the data lock object with something from your environment, and check that your download directory is set properly.
The client is designed to interface with a UI controller, which displays progress, and gives user control to pause or cancel the downloading. Individual downloads can also be cancelled, although this is as easy as deleting the .torrent file.
Note you can automatically launch the server process from the client. Either way, you need to decide if it'll run in a thread, or as a process.
This is some complicated code, so you may have to play with it to understand what it does. However it's much better than writing it from scratch !!
Good luck.
Borrowed Some Code. From this recipe: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/181905
Thanks!
This no longer works. As of BitTorrent 4.1 or so BT natively supports Twisted, as such the above recipe no longer works without some hevy modification. Currently the best idea is to subclass LaunchMany, or to just add your own layer of MultiTorrent.
See http://metamoof.net/2006/02/13/getting-bittorrent-working-under-twisted
This no longer works. As of BitTorrent 4.1 or so BT natively supports Twisted, as such the above recipe no longer works without some hevy modification. Currently the best idea is to subclass LaunchMany, or to just add your own layer of MultiTorrent.
See http://metamoof.net/2006/02/13/getting-bittorrent-working-under-twisted
History of BitTorrent vs. Twisted. As per the comments on metamoof.
The design of BitTorrent predates twisted and twisted's rawserver looks eerily similar to twisted's reactor. Of course twisted has grown well beyond what rawserver ever tried to achieve.
In fact many bugs in twisted were debugged by Greg Hazel who was responsible for integrating twisted into BitTorrent.