import sys
import wx
import os
from cStringIO import StringIO
from sha import sha
from time import strftime,localtime,time
from traceback import print_exc,print_stack
from BitTornado.bencode import bencode
from ABC.Torrent.files import TorrentFiles
from ABC.Torrent.connectmanager import TorrentConnections
from ABC.Torrent.actions import TorrentActions
from ABC.Torrent.config import TorrentConfig
from ABC.Torrent.dialogs import TorrentDialogs
from ABC.Torrent.status import TorrentStatus
from Utility.constants import *#IGNORE:W0611
fromSwapper.unicodename2unicode
from time import time
try:
True
except:
True = 1
False = 0
DEBUG = False
import threading
################################################################
#
# Class: ABCTorrent
#
# Stores information about a torrent and keeps track of its
# status
#
################################################################
class ABCTorrent:
def __init__(self, queue, src = None, dest = None, forceasklocation = False, caller = "", caller_data = None):
self.queue = queue
self.utility = self.queue.utility
self.mypref_db = self.utility.mypref_db
self.torrent_db = self.utility.torrent_db
self.list = self.utility.list
self.listindex = len(self.utility.torrents["all"])
self.src = src
self.caller = caller
self.caller_data = caller_data
self.status = TorrentStatus(self)
self.actions = TorrentActions(self)
self.dialogs = TorrentDialogs(self)
self.connection = TorrentConnections(self)
#########
self.metainfo = self.getResponse()
if self.metainfo is None:
return
# Get infohash first before doing anything else
self.infohash = sha(bencode(self.metainfo['info'])).hexdigest()
self.torrent_hash = sha(bencode(self.metainfo['info'])).digest()
self.torrentconfig = TorrentConfig(self)
# check for unicode name
self.namekey = name2unicode(self.metainfo)
# Check for valid windows filename
if sys.platform == 'win32':
fixedname = self.utility.fixWindowsName(self.metainfo['info'][self.namekey])
if fixedname:
self.metainfo['info'][self.namekey] = fixedname
# Arno: see name2unicode
self.metainfo['info']['name'] = fixedname
self.info = self.metainfo['info']
self.title = None
# Initialize values to defaults
self.files = TorrentFiles(self)
# Setup the destination
self.files.setupDest(dest, forceasklocation, caller)
if self.files.dest is None:
return
#########
# Priority "Normal"
priorities = [ self.utility.lang.get('highest'),
self.utility.lang.get('high'),
self.utility.lang.get('normal'),
self.utility.lang.get('low'),
self.utility.lang.get('lowest') ]
currentprio = self.utility.config.Read('defaultpriority', "int")
if currentprio < 0:
currentprio = 0
elif currentprio >= len(priorities):
currentprio = len(priorities) - 1
self.prio = currentprio
self.color = { 'text': None,
'bgcolor': None }
# Done flag
self.messages = { "current": "",
"log": [],
"timer": None }
self.checkedonce = False
self.totalpeers = "?"
self.totalseeds = "?"
self.peer_swarm = {} # swarm of each torrent, used to display peers on map
def addTorrentToDB(self):
# Arno: Checking for presence in the database causes some problems
# during testing sometimes, and it makes sense to update the database
# to the latest values.
#if self.torrent_db.hasTorrent(self.torrent_hash):
# return
torrent = {}
torrent['torrent_dir'], torrent['torrent_name'] = os.path.split(self.src)
torrent['relevance'] = 100*1000
torrent_info = {}
torrent_info['name'] = self.info.get(self.namekey, '')
length = 0
nf = 0
if self.info.has_key('length'):
length = self.info.get('length', 0)
nf = 1
elif self.info.has_key('files'):
for li in self.info['files']:
nf += 1
if li.has_key('length'):
length += li['length']
torrent_info['length'] = length
torrent_info['num_files'] = nf
torrent_info['announce'] = self.metainfo.get('announce', '')
torrent_info['announce-list'] = self.metainfo.get('announce-list', '')
torrent_info['creation date'] = self.metainfo.get('creation date', 0)
torrent['info'] = torrent_info
self.torrent_db.addTorrent(self.torrent_hash, torrent, new_metadata=True)
self.torrent_db.sync()
if DEBUG:
print >> sys.stderr, "add torrent to db", self.infohash, torrent_info
def addMyPref(self):
# If this is a helper torrent, don't add it as my preference
if self.caller_data is not None:
return
self.addTorrentToDB()
if self.mypref_db.hasPreference(self.torrent_hash):
return
mypref = {}
if self.files.dest:
mypref['content_dir'] = self.files.dest #TODO: check
mypref['content_name'] = self.files.filename
self.mypref_db.addPreference(self.torrent_hash, mypref)
if self.utility.abcfileframe is not None:
self.utility.abcfileframe.updateMyPref()
self.utility.buddycast.addMyPref(self.torrent_hash)
if DEBUG:
print >> sys.stderr, "add mypref to db", self.infohash, mypref
#
# Tasks to perform when first starting adding this torrent to the display
#
def postInitTasks(self):
self.utility.torrents["all"].append(self)
self.utility.torrents["inactive"][self] = 1
# Read extra information about the torrent
self.torrentconfig.readAll()
# Add a new item to the list
self.list.InsertStringItem(self.listindex, "")
# Allow updates
self.status.dontupdate = False
# Add Status info in List
self.updateColumns(force = True)
self.updateColor()
# Update the size to reflect torrents with pieces set to "download never"
self.files.updateRealSize()
# Do a quick check to see if it's finished
self.status.isDoneUploading()
self.addMyPref()
#
# As opposed to getColumnText,
# this will get numbers in their raw form for doing comparisons
#
# default is used when getting values for sorting comparisons
# (this way an empty string can be treated as less than 0.0)
#
def getColumnValue(self, colid = None, default = 0.0):
if colid is None:
colid = COL_TITLE
value = None
activetorrent = self.status.isActive(checking = False, pause = False)
try:
if colid == COL_PROGRESS: # Progress
progress = self.files.progress
if self.status.isActive(pause = False):
progress = self.connection.engine.progress
value = progress
elif colid == COL_PRIO: # Priority
value = self.prio
elif colid == COL_ETA: # ETA
if activetorrent:
if self.status.completed:
if self.connection.getSeedOption('uploadoption') == '0':
value = 999999999999999
else:
value = self.connection.seedingtimeleft
elif self.connection.engine.eta is not None:
value = self.connection.engine.eta
elif colid == COL_SIZE: # Size
value = self.files.floattotalsize
elif colid == COL_DLSPEED: # DL Speed
if activetorrent and not self.status.completed:
if self.connection.engine.hasConnections:
value = self.connection.engine.rate['down']
else:
value = 0.0
elif colid == COL_ULSPEED: # UL Speed
if activetorrent:
if self.connection.engine.hasConnections:
value = self.connection.engine.rate['up']
else:
value = 0.0
elif colid == COL_RATIO: # %U/D Size
if self.files.downsize == 0.0 :
ratio = ((self.files.upsize/self.files.floattotalsize) * 100)
else:
ratio = ((self.files.upsize/self.files.downsize) * 100)
value = ratio
elif colid == COL_SEEDS: # #Connected Seed
if activetorrent:
value = self.connection.engine.numseeds
elif colid == COL_PEERS: # #Connected Peer
if activetorrent:
value = self.connection.engine.numpeers
elif colid == COL_COPIES: # #Seeing Copies
if (activetorrent
and self.connection.engine.numcopies is not None):
value = float(0.001*int(1000*self.connection.engine.numcopies))
elif colid == COL_PEERPROGRESS: # Peer Avg Progress
if (self.connection.engine is not None
and self.connection.engine.peeravg is not None):
value = self.connection.engine.peeravg
elif colid == COL_DLSIZE: # Download Size
value = self.files.downsize
elif colid == COL_ULSIZE: # Upload Size
value = self.files.upsize
elif colid == COL_TOTALSPEED: # Total Speed
if activetorrent:
value = self.connection.engine.totalspeed
elif colid == COL_SEEDTIME: # Seeding time
value = self.connection.seedingtime
elif colid == COL_CONNECTIONS: # Connections
if activetorrent:
value = self.connection.engine.numconnections
elif colid == COL_SEEDOPTION: # Seeding option
option = int(self.connection.getSeedOption('uploadoption'))
if option == 0:
# Unlimited
value = 0.0
elif option == 1:
text = "1." + str(self.connection.getTargetSeedingTime())
value = float(text)
elif option == 2:
text = "1." + str(self.connection.getSeedOption('uploadratio'))
value = float(text)
else:
value = self.getColumnText(colid)
except:
value = self.getColumnText(colid)
if value is None or value == "":
return default
return value
#
# Get the text representation of a given column's data
# (used for display)
#
def getColumnText(self, colid):
text = None
activetorrent = self.status.isActive(checking = False, pause = False)
try:
if colid == COL_TITLE: # Title
if self.title is None:
text = self.files.filename
else:
text = self.title
elif colid == COL_PROGRESS: # Progress
progress = self.files.progress
if self.status.isActive(pause = False):
progress = self.connection.engine.progress
# Truncate the progress value rather than round down
# (will show 99.9% for incomplete torrents rather than 100.0%)
progress = int(progress * 10)/10.0
text = ('%.1f' % progress) + "%"
elif colid == COL_BTSTATUS: # BT Status
text = self.status.getStatusText()
elif colid == COL_PRIO: # Priority
priorities = [ self.utility.lang.get('highest'),
self.utility.lang.get('high'),
self.utility.lang.get('normal'),
self.utility.lang.get('low'),
self.utility.lang.get('lowest') ]
text = priorities[self.prio]
elif colid == COL_ETA and activetorrent: # ETA
value = None
if self.status.completed:
if self.connection.getSeedOption('uploadoption') == "0":
text = "(oo)"
else:
value = self.connection.seedingtimeleft
text = "(" + self.utility.eta_value(value) + ")"
elif self.connection.engine.eta is not None:
value = self.connection.engine.eta
text = self.utility.eta_value(value)
elif colid == COL_SIZE: # Size
# Some file pieces are set to "download never"
if self.files.floattotalsize != self.files.realsize:
label = self.utility.size_format(self.files.floattotalsize, textonly = True)
realsizetext = self.utility.size_format(self.files.realsize, truncate = 1, stopearly = label, applylabel = False)
totalsizetext = self.utility.size_format(self.files.floattotalsize, truncate = 1)
text = realsizetext + "/" + totalsizetext
else:
text = self.utility.size_format(self.files.floattotalsize)
elif (colid == COL_DLSPEED
and activetorrent
and not self.status.completed): # DL Speed
if self.connection.engine.hasConnections:
value = self.connection.engine.rate['down']
else:
value = 0.0
text = self.utility.speed_format(value)
elif colid == COL_ULSPEED and activetorrent: # UL Speed
if self.connection.engine.hasConnections:
value = self.connection.engine.rate['up']
else:
value = 0.0
text = self.utility.speed_format(value)
elif colid == COL_RATIO: # %U/D Size
if self.files.downsize == 0.0 :
ratio = ((self.files.upsize/self.files.floattotalsize) * 100)
else:
ratio = ((self.files.upsize/self.files.downsize) * 100)
text = '%.1f' % (ratio) + "%"
elif colid == COL_MESSAGE: # Error Message
text = self.messages["current"]
# If the error message is a system traceback, write an error
if text.find("Traceback") != -1:
sys.stderr.write(text + "\n")
elif colid == COL_SEEDS: # #Connected Seed
seeds = "0"
if activetorrent:
seeds = ('%d' % self.connection.engine.numseeds)
text = seeds + " (" + str(self.totalseeds) + ")"
elif colid == COL_PEERS: # #Connected Peer
peers = "0"
if activetorrent:
peers = ('%d' % self.connection.engine.numpeers)
text = peers + " (" + str(self.totalpeers) + ")"
elif (colid == COL_COPIES
and activetorrent
and self.connection.engine.numcopies is not None): # #Seeing Copies
text = ('%.3f' % float(0.001*int(1000*self.connection.engine.numcopies)))
elif (colid == COL_PEERPROGRESS
and activetorrent
and self.connection.engine.peeravg is not None): # Peer Avg Progress
text = ('%.1f%%'%self.connection.engine.peeravg)
elif colid == COL_DLSIZE: # Download Size
text = self.utility.size_format(self.files.downsize)
elif colid == COL_ULSIZE: # Upload Size
text = self.utility.size_format(self.files.upsize)
elif colid == COL_TOTALSPEED and activetorrent: # Total Speed
text = self.utility.speed_format(self.connection.engine.totalspeed, truncate = 0)
elif colid == COL_NAME: # Torrent Name
text = os.path.split(self.src)[1]
elif colid == COL_DEST: # Destination
text = self.files.dest
elif colid == COL_SEEDTIME: # Seeding time
value = self.connection.seedingtime
if value > 0:
text = self.utility.eta_value(value)
elif colid == COL_CONNECTIONS and activetorrent: # Connections
if self.connection.engine is not None:
text = ('%d' % self.connection.engine.numconnections)
elif colid == COL_SEEDOPTION:
value = self.connection.getSeedOption('uploadoption')
if value == "0":
# Unlimited
text = 'oo'
elif value == "1":
targettime = self.connection.getTargetSeedingTime()
text = self.utility.eta_value(targettime, 2)
elif value == "2":
text = str(self.connection.getSeedOption('uploadratio')) + "%"
except:
nowactive = self.status.isActive(checking = False, pause = False)
# Just ignore the error if it was caused by the torrent changing
# from active to inactive
if activetorrent != nowactive:
# Note: if we have an error returning the text for
# the column used to display errors, just output
# to stderr, since we don't want to cause an infinite
# loop.
data = StringIO()
print_exc(file = data)
if colid != 13:
self.changeMessage(data.getvalue(), type = "error")
else:
sys.stderr.write(data.getvalue())
if text is None:
text = ""
return text
#
# Update multiple columns in the display
# if columnlist is None, update all columns
# (only visible columns will be updated)
#
def updateColumns(self, columnlist = None, force = False):
if DEBUG:
if threading.currentThread().getName() != "MainThread":
print >> sys.stderr,"abctorrent: updateColumns thread",threading.currentThread()
print >> sys.stderr,"abctorrent: NOT MAIN THREAD"
print_stack()
if columnlist is None:
columnlist = range(self.list.columns.minid, self.list.columns.maxid)
try:
for colid in columnlist:
#print colid,
# Don't do anything if ABC is shutting down
# or minimized
if self.status.dontupdate or not self.utility.frame.GUIupdate:
if DEBUG:
print "torrent: update cols: not updating cols, GUIupdate is",self.utility.frame.GUIupdate, time()
return
# Only update if this column is currently shown
rank = self.list.columns.getRankfromID(colid)
if (rank == -1):
continue
text = self.getColumnText(colid)
if not force:
# Only update if the text has changed
try:
oldtext = self.list.GetItem(self.listindex, rank).GetText()
except:
oldtext = ""
if text != oldtext:
force = True
if force:
self.list.SetStringItem(self.listindex, rank, text)
except wx.PyDeadObjectError, msg:
print >> sys.stderr,"error updateColumns:", msg
pass
#
# Update the text and background color for the torrent
#
# colorString should be the name of a valid entry in
# the config file for a color
#
# force allows forcing the color update even if the values
# don't appear to have changed
# (needed when moving list items, since we're only
# comparing the value that we last set the color to
# not the acutal color of the list item to save time)
#
# def updateColor(self, colorString = None, force = False):
def updateColor(self, force = False):
if DEBUG:
if threading.currentThread().getName() != "MainThread":
print >> sys.stderr,"abctorrent: updateColour thread",threading.currentThread()
print >> sys.stderr,"colour NOT MAIN THREAD"
print_stack()
# Don't do anything if ABC is shutting down
if self.status.dontupdate:
return
# Don't update display while minimized/shutting down
if not self.utility.frame.GUIupdate:
return
colorString = None
if self.connection.engine is not None:
colorString = self.connection.engine.color
if colorString is None:
colorString = 'color_startup'
color = self.utility.config.Read(colorString, "color")
# Update color
if (self.utility.config.Read('stripedlist', "boolean")) and (self.listindex % 2):
bgcolor = self.utility.config.Read('color_stripe', "color")
else:
# Use system specified background:
bgcolor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)
# Only update the color if it has changed
# (or if the force flag is set to True)
if (force
or self.color['bgcolor'] is None
or bgcolor != self.color['bgcolor']
or self.color['text'] is None
or color != self.color['text']):
try:
item = self.list.GetItem(self.listindex)
item.SetTextColour(color)
item.SetBackgroundColour(bgcolor)
self.list.SetItem(item)
self.color['text'] = color
self.color['bgcolor'] = bgcolor
except:
self.color['text'] = None
self.color['bgcolor'] = None
#
# Update the fields that change frequently
# for active torrents
#
def updateSingleItemStatus(self):
if threading.currentThread().getName() != "MainThread":
print >> sys.stderr,"abctorrent: updateSingleItem thread",threading.currentThread()
print >> sys.stderr,"abctorrent: NOT MAIN THREAD"
print_stack()
# Ignore 4, 5, 7, 9, 12, 13, 18, 22, 25
# Do check to see if we're done uploading
self.status.isDoneUploading()
self.updateColumns([COL_PROGRESS,
COL_BTSTATUS,
COL_ETA,
COL_DLSPEED,
COL_ULSPEED,
COL_SEEDS,
COL_PEERS,
COL_COPIES,
COL_PEERPROGRESS,
COL_ULSIZE,
COL_TOTALSPEED,
COL_NAME,
COL_SEEDTIME,
COL_CONNECTIONS])
self.updateColor('color_startup')
#
# Get metainfo for the torrent
#
def getResponse(self):
if self.status.isActive():
#active process
metainfo = self.connection.engine.dow.getResponse()
else:
#not active process
metainfo = self.utility.getMetainfo(self.src)
return metainfo
#
# Get information about the torrent to return to the webservice
#
def getInfo(self, fieldlist = None):
# Default to returning all fields
if fieldlist is None:
fieldlist = range(self.list.columns.minid, self.list.columns.maxid)
try :
retmsg = ""
for colid in fieldlist:
retmsg += self.getColumnText(colid) + "|"
retmsg += self.infohash + "\n"
return retmsg
except:
# Should never get to this point
return "|" * len(fieldlist) + "\n"
#
# Update the torrent with new scrape information
#
def updateScrapeData(self, newpeer, newseed, message = ""):
if threading.currentThread().getName() != "MainThread":
print >> sys.stderr,"abctorrent: updateScrapeData thread",threading.currentThread()
print >> sys.stderr,"abctorrent: NOT MAIN THREAD"
print_stack()
self.actions.lastgetscrape = time()
self.totalpeers = newpeer
self.totalseeds = newseed
self.updateColumns([COL_SEEDS, COL_PEERS])
if message != "":
if DEBUG:
print >> sys.stderr,"message: " + message
if message == self.utility.lang.get('scraping'):
msgtype = "status"
elif message == self.utility.lang.get('scrapingdone'):
msgtype = "status"
message += " (" + \
self.utility.lang.get('column14_text') + \
": " + \
str(self.totalseeds) + \
" / " + \
self.utility.lang.get('column15_text') + \
": " + \
str(self.totalpeers) + \
")"
else:
msgtype = "error"
self.changeMessage(message, msgtype)
# Update detail window
if self.dialogs.details is not None:
self.dialogs.details.detailPanel.updateFromABCTorrent()
def changeMessage(self, message = "", type = "clear"):
if threading.currentThread().getName() != "MainThread":
print >> sys.stderr,"abctorrent: updateScrapeData thread",threading.currentThread()
print >> sys.stderr,"abctorrent: NOT MAIN THREAD"
print_stack()
# Clear the error message
if type == "clear":
self.messages["current"] = ""
self.updateColumns([COL_MESSAGE])
return
if not message:
return
now = time()
self.messages["lasttime"] = now
if type == "error" or type == "status":
self.messages["current"] = strftime('%H:%M', localtime(now)) + " - " + message
self.updateColumns([COL_MESSAGE])
self.messages["log"].append([now, message, type])
if self.dialogs.details is not None:
self.dialogs.details.messageLogPanel.updateMessageLog()
def makeInactive(self, update = True):
self.files.updateProgress()
if self.status.value == STATUS_HASHCHECK:
self.status.updateStatus(self.actions.oldstatus)
elif self.status.value == STATUS_STOP:
pass
elif self.connection.engine is not None:
# Ensure that this part only gets called once
self.status.updateStatus(STATUS_QUEUE)
# Write out to config
self.torrentconfig.writeAll()
if update:
self.updateSingleItemStatus()
def getTitle(self, kind = "current"):
title = self.getColumnText(COL_TITLE)
if kind == "original":
title = self.metainfo['info'][self.namekey]
elif kind == "torrent":
torrentfilename = os.path.split(self.src)[1]
torrentfilename = torrentfilename[:torrentfilename.rfind('.torrent')]
title = torrentfilename
elif kind == "dest":
if self.files.isFile():
destloc = self.files.dest
else:
destloc = self.files.getProcDest(pathonly = True, checkexists = False)
title = os.path.split(destloc)[1]
return title
def changeTitle(self, title):
if title == self.files.filename:
self.title = None
else:
self.title = title
self.torrentconfig.writeNameParams()
self.updateColumns([COL_TITLE])
# Change the priority for the torrent
def changePriority(self, prio):
self.prio = prio
self.updateColumns([COL_PRIO])
self.torrentconfig.writePriority()
# Things to do when shutting down a torrent
def shutdown(self):
# Set shutdown flag to true
self.status.dontupdate = True
self.torrentconfig.writeAll()
# Delete Detail Window
########################
try:
if self.dialogs.details is not None:
self.dialogs.details.killAdv()
except wx.PyDeadObjectError:
pass
# # (if it's currently active, wait for it to stop)
# self.connection.stopEngine(waitForThread = True)
self.connection.stopEngine()
# If this is a helper torrent, remove all traces
if self.caller_data is not None:
if self.caller_data.has_key('coordinator_permid'):
try:
os.remove(self.src)
os.remove(self.torrentconfig.filename)
except:
pass
self.files.removeFiles()
if self.utility.abcquitting:
# Normally done in procREMOVE, except when stopping client
self.utility.torrents["all"].remove(self)
del self.utility.torrents["inactive"][self]
|