#!/usr/bin/python ############################################################################## # Replay TV Client # # (c) 2003 Michael Rothwell # Licensed under the GPL version 2.0. # See http://www.fsf.org/licenses/gpl.html for more license information. # # ABOUT # This program detects ReplayTV units on the local network using UPnP # (Universal Plug and Play), and provides a GUI for getting the program # guide and downloading MPEG files. # Currently, it supports only the 5000 series. # # # REQUIREMENTS: # Windows: # You will need Python version 2 or later. This is developed with Python 2.2. # You can download ActivePython for Windows from http://www.activestate.com # You will need wxPython. This can be downloaded from http://www.wxpython.org # # Linux: # If you are running linux, you will need Python 2 or later (e.g., version 2.2) # and wxPython. Redhat 8.0 includes an appropriate version of Python. It does # not include wxPython. You will need to download and install it. # # Other OS: # Same as above -- Python and wxPython. # # # INSTALLATION: # ALL: # Install the requirements (see above). Put this file somewhere. # Linux, other Unix: # chmod 0755 ./ReplayTVClient.py # # # USAGE: # Windows: # double-click this file in Explorer. # Linux, other Unix: # ./ReplayTVClient.py (if your python interpreter is at /usr/bin/python) # or # python ./ReplayTVClient.py (if your python lives somewhere else) # # EDITING: # A tab is four spaces. Send patches to rothwell@holly-springs.nc.us # # # CVS: # $Id: ReplayTVClient.py,v 1.1 2003/09/04 14:08:09 rothwell Exp $ # ############################################################################## ############################################################################## # Imports -- required modules ############################################################################## from struct import pack, unpack, calcsize import string, time, random, md5, httplib import socket, asyncore, select try: from wxPython import wx except: print "Failed to import wx from wxPython" try: from wxPython.wx import * except: print "Failed to import wxPython.wx" try: from wxPython import * except: print "Failed to import wxPython" try: from wxPython.html import * except: print "Failed to import wxPython.html" try: import wxPython.lib.wxpTag except: print "Failed to import wxPython.lib.wxpTag" try: from wxPython.lib.mixins.listctrl import wxColumnSorterMixin, wxListCtrlAutoWidthMixin except: print "Failed to import wxPython mixins" from threading import * import xml.dom.minidom ############################################################################## # CVS Revision Information ############################################################################## BUILD = "$Id: ReplayTVClient.py,v 1.1 2003/09/04 14:08:09 rothwell Exp $" aBUILD = BUILD.split() REVISION = "1.%s" % aBUILD[2][aBUILD[2].find('.')+1:] BUILDATE = aBUILD[3] ############################################################################## # GuideParser Classes ############################################################################## class ReplayGuide: def __init__(self, guidedata): self.guidedata = guidedata self.header = GuideSnapshotHeader(self.guidedata) print "ReplayTV Version: %d.%d" %(self.header['major'],self.header['minor']) self.major = self.header['major'] self.minor = self.header['minor'] if (self.major == 0 and self.minor == 2): self.major=5 self.minor=2 self.showoffset = self.header['showoffset'] self.shows = [] self.ReplayVersion = "xxxx" self.guidesize = len(self.guidedata) self.showsize = 0 # replay 5000 if self.major==5: self.ReplayVersion = "5xxx" log ("Replay %s"%self.ReplayVersion) log ("ShowOffset %d"%self.showoffset) self.showsize = ReplayShow().size() count = 0 while count < 100 and (self.showoffset + count * self.showsize) < self.guidesize: try: show = ReplayShow(self.guidedata, self.showoffset + count * self.showsize) log("appending show %d"%(count + 1)) self.shows.append(show) count = count + 1 except: break log("%d shows"%count) return # replay 4000 if self.major==3: self.ReplayVersion = "4xxx" log ("Replay %s"%self.ReplayVersion) log ("ShowOffset %d"%self.showoffset) self.showsize = ReplayShow4000().size() count = 0 while count < 100 and (self.showoffset + count * self.showsize) < self.guidesize: try: show = ReplayShow4000(self.guidedata, self.showoffset + count * self.showsize) log("appending show %d"%(count + 1)) self.shows.append(show) count = count + 1 except: break log("%d shows"%count) return print "Unknown Replay version: %d.%d"%(self.major,self.minor) # base class class ReplayStruct: def __getitem__(self,name): return self.data[self.fields[name]] def decode(self): if (self.guideData): try: self.data = unpack(self.format,self.guideData[self.start:self.start+calcsize(self.format)]); except: log("ERROR: could not parse guide data") self.data=None else: self.data=None def size(self): return calcsize(self.format) # GuideSnapshotHeader class GuideSnapshotHeader(ReplayStruct): def __init__(self,guideData=None, start=0): self.format = "!HHLLLLLLL" self.start = start self.guideData = guideData self.fields = { 'major':0, 'minor':1, 'structuresize':2, 'replaychannels':3, 'replaychannels2':4, 'groupdataoffset':5, 'channeloffset':6, 'showoffset':7, 'flags':8 } self.decode() if self.data: log("GuideSnapshotHeader: (start: %d size:%d): %s"%(start,self.size(),str(self.data))) if (self.data[0] == 0 and self.data[1] == 2): self.format="!HHLLLLLLLLLLLLLLL" self.fields = { 'major':0, 'minor':1, 'structuresize':2, 'replaychannels':5, 'replaychannels2':6, 'groupdataoffset':8, 'channeloffset':9, 'showoffset':10, 'flags':14 } self.decode() # GroupData class GroupData(ReplayStruct): def __init__(self,guideData=None, start=32): self.format = "!LL32L32L512s" self.start = start self.guideData = guideData self.fields = { 'structuresize':0, 'categories':1, 'category':2} self.decode() if (self.data): idx = self.data[2:2+self.data[1]] self.idx={} y=self.data[34:34+32] x=self.data[66] categories=[] categories.append("All Shows") for i in range(0,self.data[1]): cat = x[y[i]:x.find('\0',y[i])] categories.append(cat) self.idx[1 << idx[i]]=cat self.idx[0]="All Shows" self.data=tuple([self.data[0],self.data[1],categories]) log("GroupData: (start: %d size:%d): %s"%(start,self.size(),str(self.data))) def category(self,index): try: return self.idx[index] except: print "no category index %d" % index return None # ThemeInfo class ThemeInfo(ReplayStruct): def __init__(self,guideData=None, start=0): self.format = "!LLL48s" self.start = start self.guideData = guideData self.fields = { 'flags':0, 'suzuki_id':1, 'thememinutes':2, 'searchstring':3} self.decode() if (self.data): tmp=[] tmp.append(self.data[0]) tmp.append(self.data[1]) tmp.append(self.data[2]) tmp.append(self.data[3][:self.data[3].find('\0')]) self.data=tuple(tmp) log("ThemeInfo: (start: %d size:%d): %s"%(start,self.size(),str(self.data))) # ProgramInfo class ProgramInfo(ReplayStruct): def __init__(self,guideData=None, start=0): self.format = "!LLLLLLLHBBBBHBBBBBBBB228s" self.start = start self.guideData = guideData self.fields = { 'structuresize':0, 'autorecord':1, 'isvalid':2, 'tuning':3, 'flags':4, 'eventtime':5, 'tmsID':6, 'minutes':7, 'genre1':8, 'genre2':9, 'genre3':10, 'genre4':11, 'recLen':12, 'titleLen':13, 'episodeLen':14, 'descriptionLen':15, 'actorLen':16, 'guestLen':17, 'suzukiLen':18, 'producerLen':19, 'directorLen':20, 'description':21} self.decode() self.fields = { 'autorecord':0, 'channel':1, 'flags':2, 'eventtime':3, 'eventcode':4, 'tmsID':5, 'minutes':6, 'genre1':7, 'genre2':8, 'genre3':9, 'genre4':10, 'title':11, 'episode':12, 'description':13, 'actor':14, 'guest':15, 'suzuki':16, 'producer':17, 'director':18} if self.data: log("ProgramInfo (RAW): (start: %d size:%d): %s"%(start,self.size(),str(self.data))) tmp=[] tmp.append(self.data[1]) tmp.append(self.data[3]) tmp.append(self.data[4]) tmp.append(self.data[5]) tmp.append(self.data[5]) tmp.append(long(self.data[6])) tmp.append(self.data[7]) tmp.append(self.data[8]) tmp.append(self.data[9]) tmp.append(self.data[10]) tmp.append(self.data[11]) tmp[3]=time.localtime(tmp[3]) desc = str(self.data[21]) offset = 0 # find real beginning if ord(desc[0]) < 32: desc = desc[4:] if ord(desc[0]) < 32: desc = desc[4:] # translate strange "smart quotes" res = "" for offset in range(0,len(desc)): c = desc[offset] try: if ord(desc[offset]) == 145: c = "'" if ord(desc[offset]) == 146: c = "'" if ord(desc[offset]) == 147: c = '"' if ord(desc[offset]) == 148: c = '"' except: pass res = res + c desc = res # parse out show data tmp.append(desc[:desc.find('\0')]) desc = desc[1+desc.find('\0'):] tmp.append(desc[:desc.find('\0')]) desc = desc[1+desc.find('\0'):] tmp.append(desc[:desc.find('\0')]) desc = desc[1+desc.find('\0'):] tmp.append(desc[:desc.find('\0')]) desc = desc[1+desc.find('\0'):] tmp.append(desc[:desc.find('\0')]) desc = desc[1+desc.find('\0'):] tmp.append(desc[:desc.find('\0')]) desc = desc[1+desc.find('\0'):] tmp.append(desc[:desc.find('\0')]) desc = desc[1+desc.find('\0'):] tmp.append(desc[:desc.find('\0')]) self.data = tuple(tmp) log("ProgramInfo: (start: %d size:%d): %s"%(start,self.size(),str(self.data))) # ChannelInfo class ChannelInfo(ReplayStruct): def __init__(self,guideData=None, start=0): self.format = "!LLLLHBB16s32s8sL" self.start = start self.guideData = guideData self.fields = { 'structuresize':0, 'usetuner':1, 'isvalid':2, 'tmsID':3, 'channel':4, 'device':5, 'tier':6, 'channelname':7, 'channellabel':8, 'cablesystem':9, 'channelindex':10} self.decode() if self.data: tmp=[] for i in range(0,11): tmp.append(self.data[i]) tmp[7]=tmp[7][:tmp[7].find('\0')].strip() tmp[8]=tmp[8][:tmp[8].find('\0')].strip() tmp[9]=tmp[9][:tmp[9].find('\0')].strip() self.data=tuple(tmp) log("ChannelInfo (start: %d size:%d): %s"%(start,self.size(),str(self.data))) # ReplayChannel # starts with a ReplayShow, then a ThemeInfo class ReplayChannel(ReplayStruct): def __init__(self,guideData=None, start=0): self.format = "!%ds%dsLLLLLLBBBBQ48sLLLLLLLLQLLLL" % (ReplayShow().size(), ThemeInfo().size()) self.start = start self.guideData = guideData self.ctypes = {1:'Recurring',2:'Theme',3:'Single'} self.fields = { 'created':2, 'category':3, 'channeltype':4, 'quality':5, 'keep':6, 'stored':7, 'daysofweek':8, 'afterpadding':9, 'beforepadding':10, 'flags':11, 'timereserved':12, 'showlabel':13, 'unknown1':14, 'unknown2':15, 'unknown3':16, 'unknown4':17, 'unknown5':18, 'unknown6':19, 'unknown7':20, 'unknown8':21, 'allocatedspace':22, 'unknown9':23, 'unknown10':24, 'unknown11':25, 'unknown12':26} self.decode() if self.data: self.ReplayShow = ReplayShow(self.data[0]) self.ThemeInfo = ThemeInfo(self.data[1]) log("ReplayChannel: (start: %d size:%d): %s"%(start,self.size(),str(self.data))) def ChannelType(self): if self.data: return self.ctypes[self.data[4]] else: return None # ReplayShow # has a ChannelInfo and a ProgramInfo after playbackflags class ReplayShow(ReplayStruct): def __init__(self,guideData=None, start=0): self.format = "!LLLLLL%ds%dsLLLLLLLLLLLLHBBQQ68s" % (ChannelInfo().size(), ProgramInfo().size()) self.start = start self.mpegsize = 0 self.guideData = guideData self.fields = { 'created':0, 'recorded':1, 'inputsource':2, 'quality':3, 'guaranteed':4, 'playbackflags':5, 'sChannelInfo':6, 'sProgramInfo':7, 'ivsstatus':8, 'guideid':9, 'downloadid':10, 'timessent':11, 'seconds':12, 'GOP_count':13, 'GOP_highest':14, 'GOP_last':15, 'checkpointed':16, 'intact':17, 'upgradeflag':18, 'instance':19, 'unused':20, 'beforepadding':21, 'afterpadding':22, 'indexsize':23, 'mpegsize':24, 'reserved':25} self.decode() if (self.data): self.ChannelInfo = ChannelInfo(self.data[6]) self.ProgramInfo = ProgramInfo(self.data[7]) log("ReplayShow: (start: %d size:%d): %s"%(start,self.size(),str(self.data))) # ReplayShow # has a ChannelInfo and a ProgramInfo after playbackflags class ReplayShow4000(ReplayStruct): def __init__(self,guideData=None, start=0): self.format = "!LLLLLL%ds%dsLLLLLLLLLLLLHBBQQ" % (ChannelInfo().size(), ProgramInfo().size()) self.start = start self.mpegsize = 0 self.guideData = guideData self.fields = { 'created':0, 'recorded':1, 'inputsource':2, 'quality':3, 'guaranteed':4, 'playbackflags':5, 'sChannelInfo':6, 'sProgramInfo':7, 'ivsstatus':8, 'guideid':9, 'downloadid':10, 'timessent':11, 'seconds':12, 'GOP_count':13, 'GOP_highest':14, 'GOP_last':15, 'checkpointed':16, 'intact':17, 'upgradeflag':18, 'instance':19, 'unused':20, 'beforepadding':21, 'afterpadding':22, 'indexsize':23, 'mpegsize':24, 'reserved':25} self.decode() if (self.data): self.ChannelInfo = ChannelInfo(self.data[6]) self.ProgramInfo = ProgramInfo(self.data[7]) log("ReplayShow: (start: %d size:%d): %s"%(start,self.size(),str(self.data))) ############################################################################## # Replay Utility Functions ############################################################################## # rtv_from_u16 def rtv_from_u16(val): ret = [] ret.append((val & 0xff00) >> 8) ret.append(val & 0x00ff) return ret # rtv_from_u32 def rtv_from_u32(val): ret = [] tmp = rtv_from_u16((val & 0xffff0000) >> 16) for i in tmp: ret.append(i) tmp = rtv_from_u16(val & 0x0000ffff) for i in tmp: ret.append(i) return ret # rtv_from_u64 def rtv_from_u64(val): ret = [] tmp = rtv_from_u32((val & 0xffffffff00000000) >> 32) for i in tmp: ret.append(i) tmp = rtv_from_u32(val & 0x00000000ffffffff) for i in tmp: ret.append(i) return ret # cryptblock def cryptblock(key, block): ret=[] for i in block: key = key * 0xb8f7 + 0x15bb9 i = (i ^ key) % 256 ret.append(i) return ret; # rtv_checksum def rtv_checksum(text, num): m = md5.new() m.update(text) if num==0: for i in (0x41,0x47,0xc8,0x09, 0xba,0x3c,0x99,0x6a, 0xda,0x09,0x9a,0x0f, 0xc0,0xd3,0x47,0xca, 0xd1,0x95,0x81,0x19, 0xab,0x17,0xc6,0x5f, 0xad,0xea,0xe5,0x75, 0x9c,0x49,0x18,0xa5, 0xdf,0x35,0x46,0x5b, 0x78,0x0e,0xcb,0xc7, 0x8c,0x3e,0xf4,0x90, 0xa2,0xb7,0x8e,0x00, 0x53,0x8d,0x4c,0xab, 0x13,0xa5,0x16,0x00, 0xff,0xb8,0x4b,0x20, 0x29,0x22,0x9d,0xee): m.update(chr(i)) else: for i in (0xda,0x76,0x5c,0xd4, 0x34,0xc3,0xd7,0x2c, 0xac,0x40,0xb8,0xd8, 0x59,0xbc,0x59,0x34, 0xaa,0xbf,0x89,0xbd, 0x85,0xe8,0x40,0x27, 0x78,0x2b,0x18,0x6e, 0xa6,0x6e,0x5a,0xc6, 0xda,0xe3,0x86,0x84, 0x40,0x14,0x2a,0x23, 0x4f,0x5d,0x38,0x5e, 0x7f,0xd9,0x73,0x7d, 0xe4,0x80,0x3d,0x21, 0x28,0x41,0xf1,0xb2, 0x96,0x43,0x2b,0xcc, 0x0c,0x9d,0x26,0xb9): m.update(chr(i)) return m.digest() # rtv_crypt def rtv_crypt(text, num): ret = "" key = random.randint(0,32767) obfusc = random.randint(0,32767) t = long(time.time()) c_head = [] c_data = [] e_key = rtv_from_u32(key ^ 0xcb0baf47) e_obf = rtv_from_u32(obfusc); c_head.append(e_obf[3]) c_head.append(e_key[2]) c_head.append(e_key[0]) c_head.append(e_obf[2]) c_head.append(e_key[1]) c_head.append(e_obf[1]) c_head.append(e_obf[0]) c_head.append(e_key[3]) for i in rtv_from_u32(0x42ffdfa9): c_data.append(i) for i in rtv_from_u32(t): c_data.append(i) for i in text: c_data.append(ord(i)) c_data = cryptblock(key, c_data) cs_data="" for i in c_data: cs_data = cs_data + chr(i) c_csum = rtv_checksum(cs_data,num) ret = "" for i in c_head: ret = ret + chr(i) ret = ret + c_csum[0:16] ret = ret + cs_data return ret # hex_encode def hex_encode(text): t="" for i in range(0,len(text)): t = t + "%02x" % ord(text[i]) return t # guideURL def guideUrl(serial="RTV4080K0000000000"): return "/http_replay_guide-get_snapshot?guide_file_name=%s&serial_no=%s" % (long(time.time()), serial) # mpegUrl def mpegUrl(eventcode): text='name="/Video/%s.mpg"' % eventcode ctext = rtv_crypt(text,10) t = hex_encode(ctext) path="/httpfs-readfile?__Q_=%s" % t return path # lsUrl def lsUrl(path="/Video"): text='name="%s"' % path ctext = rtv_crypt(text,10) t = hex_encode(ctext) path="/httpfs-ls?__Q_=%s" % t return path # fstatUrl def fstatUrl(path): text='name="%s"' % path ctext = rtv_crypt(text,10) t = hex_encode(ctext) path="/httpfs-fstat?__Q_=%s" % t return path # getText def getText(nodelist): rc = "" for node in nodelist: if node.nodeType == node.TEXT_NODE: rc = rc + node.data return rc ############################################################################## # MPEGFetcher (retrieves mpegs, sends events to GUI) ############################################################################## class MPEGFetcher(Thread): def __init__(self, addr, mpegid, outfile, showid): Thread.__init__(self) self.running = 1 self.addr = addr self.mpegid = mpegid self.outfile = outfile self.showid = showid if outfile==None: self.outfile = '%s.mpg'%mpegid self.path = '/Video/%s.mpg'%mpegid try: self.savefileto = file(self.outfile,"wb") except: print "ERROR: COULD NOT OPEN %s FOR WRITING" % self.outfile log("ERROR: COULD NOT OPEN %s FOR WRITING" % self.outfile) self.savefileto = None wxPostEvent(app.frame,ResultEvent( ('MPEGdataAborted',self.addr,self.amt_read, self.outfile, self.showid, xfer_rate) )) return self.headers = "" self.waiting_for_headers = 1 self.amt_read = 0 self.request = "GET %s HTTP/1.0\r\n" % mpegUrl(mpegid) self.request = self.request + "Accept-Encoding: text/plain\r\n" self.request = self.request + "User-Agent: Replay-HTTPFS/1\r\n" self.request = self.request + "Authorization: Basic Uk5TQmFzaWM6QTd4KjgtUXQ=\r\n" self.request = self.request + "Host: %s\r\n\r\n" % addr log(self.request) self.start() def run(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((self.addr,80)) self.socket.setblocking(0) self.socket.send(self.request) self.waiting_for_headers=1 start_time = time.time() xfer_rate = 0 last_time = 0 while(self.running): ready_to_read, ready_to_write, in_error = select.select([self.socket],[],[], 1) if self.socket in ready_to_read: try: data = self.socket.recv(1000000) except: print "read error fetching mpeg" if len(data)==0 and self.waiting_for_headers==0: #print "the end?" break if (self.waiting_for_headers): self.headers = self.headers + data h = self.headers.find("\r\n\r\n") if (h): self.waiting_for_headers = 0 #print "got headers: %s" % self.headers[:h] log("got headers: %s" % self.headers[:h]) data = data[h+4:] h = data.find('\n') data = data[h+1:] #h = data.find('\n') #data = data[h+1:] self.amt_read = self.amt_read + len(data) try: self.savefileto.write(data) except: wxPostEvent(app.frame,ResultEvent( ('MPEGdataAborted',self.addr,self.amt_read, self.outfile, self.showid, xfer_rate) )) break else: self.amt_read = self.amt_read + len(data) try: self.savefileto.write(data) except: wxPostEvent(app.frame,ResultEvent( ('MPEGdataAborted',self.addr,self.amt_read, self.outfile, self.showid, xfer_rate) )) break this_time = long(time.time()) try: xfer_rate = long(self.amt_read/(this_time - start_time))/1024 except: xfer_rate = 0 if this_time > last_time: last_time = this_time wxPostEvent(app.frame,ResultEvent( ('gotMPEGdata',self.addr,self.amt_read, self.outfile, self.showid, xfer_rate) )) self.socket.shutdown(2) self.socket.close() self.savefileto.close() if (self.running): wxPostEvent(app.frame,ResultEvent( ('gotALLMPEGdata',self.addr,self.amt_read, self.outfile, self.showid, xfer_rate) )) else: wxPostEvent(app.frame,ResultEvent( ('MPEGdataAborted',self.addr,self.amt_read, self.outfile, self.showid, xfer_rate) )) ############################################################################## # rtv_fstat -- fstats a file ############################################################################## class rtv_fstat(Thread): def __init__(self, addr, mpegid, showid): Thread.__init__(self) self.running = 1 self.addr = addr self.mpegid = mpegid self.path = "/Video/%s.mpg"%mpegid self.showid = showid self.headers = "" self.waiting_for_headers = 1 self.amt_read = 0 self.request = "GET %s HTTP/1.0\r\n" % fstatUrl(self.path) self.request = self.request + "Accept-Encoding: text/plain\r\n" self.request = self.request + "User-Agent: Replay-HTTPFS/1\r\n" self.request = self.request + "Authorization: Basic Uk5TQmFzaWM6QTd4KjgtUXQ=\r\n" self.request = self.request + "Host: %s\r\n\r\n" % addr self.result="" self.start() def run(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self.socket.connect((self.addr,80)) except: print "ERROR: Could not connect to %s" % self.addr return self.socket.setblocking(0) self.socket.send(self.request) self.waiting_for_headers=1 last_time = 0 log(self.request) while(self.running): ready_to_read, ready_to_write, in_error = select.select([self.socket],[],[], 1) if self.socket in ready_to_read: data = self.socket.recv(1000000) if len(data)==0 and self.waiting_for_headers==0: #print "the end?" break log(data) if (self.waiting_for_headers): self.headers = self.headers + data h = self.headers.find("\r\n\r\n") if (h): self.waiting_for_headers = 0 #print "got headers: %s" % self.headers[:h] data = data[h+4:] #h = data.find('\n') #data = data[h+1:] #h = data.find('\n') #data = data[h+1:] self.amt_read = self.amt_read + len(data) self.result = self.result + data else: self.amt_read = self.amt_read + len(data) self.result = self.result + data this_time = long(time.time()) if this_time > last_time: last_time = this_time #wxPostEvent(app.frame,ResultEvent( ('gotMPEGdata',self.addr,self.amt_read, self.outfile) )) self.socket.shutdown(2) self.socket.close() if (self.running): wxPostEvent(app.frame,ResultEvent( ('fstat', self.addr, self.mpegid, self.result, self.showid) )) ############################################################################## # GuideFetcher (retrieves guide, sends event to GUI) ############################################################################## class GuideFetcher(Thread): def __init__(self, addr, serial=None): Thread.__init__(self) self.addr = addr self.serial = serial self.start() def run(self): try: conn = httplib.HTTPConnection('%s:80' % self.addr) conn.request("GET", guideUrl(self.serial)) res = conn.getresponse() guideData = res.read() f = guideData.find("#####ATTACHED_FILE_START#####") guideData = guideData[f+29:] wxPostEvent(app.frame,ResultEvent( ('updateGuideData',self.addr,guideData) )) except: print "failed to fetch guide" ############################################################################## # UPnP ( looks for Replay units, sends events to GUI ) # FIXME: move manipulation of replay_device_list to gui thread ############################################################################## class Replay_UPnP(Thread): def __init__(self): Thread.__init__(self) self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.setblocking(0) self.running = 1 self.start() def send_search(self): self.socket.sendto("M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN: \"ssdp:discover\"\r\nST: urn:replaytv-com:device:ReplayDevice:1\r\nMX: 3\r\n\r\n", 0,('239.255.255.250', 1900)) def run(self): ticks = 0 while(self.running): if ticks % 60 == 0: self.send_search() ticks = ticks + 1 ready_to_read, ready_to_write, in_error = select.select([self.socket],[],[], 1) if self.socket in ready_to_read: data, addr = self.socket.recvfrom(512) p = data.find('LOCATION: ') loc = '' if (p): loc = data[p+10:] p = loc.find('\n') if (p): loc = loc[:p] loc = loc.strip() device_info='' fn='' sn='' if (loc): try: conn = httplib.HTTPConnection('%s:80' % addr[0]) conn.request("GET", loc) res = conn.getresponse() device_info = res.read() p = device_info.find('') def OnSize(self, event): w,h = self.GetClientSizeTuple() self.PanelA.SetDimensions(0,0,self.DeviceListWidth,h) #self.PanelB.SetDimensions(self.DeviceListWidth,0,self.DeviceListWidth,h) self.PanelC.SetDimensions(self.DeviceListWidth,0,w-self.DeviceListWidth,h/2) self.PanelD.SetDimensions(self.DeviceListWidth,h/2,w-self.DeviceListWidth,h/2) def OnUpdate(self, event): if event.data[0]=='addReplyDevice': app.frame.PanelA.InsertStringItem(event.data[1], event.data[2]) if event.data[0]=='setReplyDeviceName': app.frame.PanelA.SetItemText(event.data[1], event.data[2]) if event.data[0]=='updateGuideData': addr = event.data[1] guideData = event.data[2] replay_device_list['%s_guide'%addr] = guideData #print "got guide data for event.data[1]" #print '%s_guide:'%addr, len(replay_device_list['%s_guide'%addr]) replay_device_list['%s_fetchingGuide'%addr] = 0 tmp = replay_device_list[addr] self.updateGuideDisplay(tmp['id']) if event.data[0]=='gotMPEGdata': addr = event.data[1] amt_read = event.data[2] outfile = event.data[3] showid = event.data[4] xfer_rate = event.data[5] show = self.showindex[showid] size = show.mpegsize show.amt_read = amt_read try: self.saving_progressbar.SetRange(1000) self.saving_progressbar.SetValue(1000*amt_read/size) self.saving_progress_speedtext.SetLabel("%s KB/sec"%xfer_rate) self.saving_progresstext.SetLabel("%s of %s KB retrieved"%( long(amt_read/1024), long(size/1024))) except: print "oops" if event.data[0]=='gotALLMPEGdata': addr = event.data[1] amt_read = event.data[2] outfile = event.data[3] showid = event.data[4] xfer_rate = event.data[5] show = self.showindex[showid] size = show.mpegsize show.amt_read = amt_read try: self.saving_progressbar.SetRange(1000) self.saving_progressbar.SetValue(1000*amt_read/size) self.saving_progresstext.SetLabel("Done; %s of %s KB retrieved"%( long(amt_read/1024), long(size/1024))) self.saving_btn.SetLabel(" Close ") log("%s of %s KB retrieved"%( long(amt_read/1024), long(size/1024))) except: pass try: replay_device_list['_down%s'%addr] = 0 except: print "oops" if event.data[0]=='MPEGdataAborted': addr = event.data[1] amt_read = event.data[2] outfile = event.data[3] showid = event.data[4] xfer_rate = event.data[5] show = self.showindex[showid] size = show.mpegsize show.amt_read = amt_read try: self.saving_progressbar.SetRange(1000) self.saving_progressbar.SetValue(1000*amt_read/size) except: pass print "%s ABORTED, saved %s/%s bytes to %s" % (addr, amt_read, size, outfile) log("%s ABORTED, saved %s/%s bytes to %s" % (addr, amt_read, size, outfile)) try: replay_device_list['_down%s'%addr] = 0 except: print "oops" if event.data[0]=='fstat': addr = event.data[1] mpegid = event.data[2] result = event.data[3] showid = event.data[4] ary=result.split('\n') s="" for i in ary: if i[:5]=='size=': s = i break ary = s.split('=') size = 0 try: size = ary[1] except: print "ERROR: COULD NOT FSTAT FILE" size = 0 #print "Size: %s"%size show = self.showindex[showid] show.mpegsize=long(size) self.updateShowDisplay(show) def onReplayCategorySelected(self, event): print 'onReplayCategorySelected', event.data def onReplayShowSelected(self, event): #print 'onReplayShowSelected', event.m_itemIndex show = self.showindex[self.PanelC.GetItemData(event.m_itemIndex)] addr = replay_device_list['_id%s'%self.SelectedReplayDeviceID] showindex = replay_device_list['%s_shows'%addr] if show.mpegsize == 0: f = rtv_fstat(addr,show['recorded'],self.PanelC.GetItemData(event.m_itemIndex)) self.updateShowDisplay(show) def updateShowDisplay(self, show): html = '' html = html + '' html = html + ''%show.ProgramInfo['title'] html = html + ''%show.ProgramInfo['episode'] html = html + ''%show.ProgramInfo['description'] html = html + ''%(show.ChannelInfo['channelname'], show.ChannelInfo['channellabel'],show.ChannelInfo['channel']) html = html + ''%time.asctime(show.ProgramInfo['eventtime']) html = html + ''%(show['recorded'], show.mpegsize) html = html + ''%show.ProgramInfo['minutes'] html = html + ''%show.ProgramInfo['actor'] html = html + ''%show.ProgramInfo['guest'] html = html + ''%show.ProgramInfo['producer'] html = html + ''%show.ProgramInfo['director'] html = html + '
Title:%s
Episode:%s
Description:%s
Channel:%s (%s), %d
Recorded:%s
MPEG file:%s.mpg (%s bytes)
Duration:%s minutes
Actor(s):%s
Guest(s):%s
Producer(s):%s
Director(s):%s
' html = html + '
Double-click a show to download the MPG file' html = html + '' self.PanelD.SetPage(html) def onReplayShowActivated(self, event): show = self.showindex[self.PanelC.GetItemData(event.m_itemIndex)] addr = replay_device_list['_id%s'%self.SelectedReplayDeviceID] replay_device_list['_down%s'%addr]=1 showindex = replay_device_list['%s_shows'%addr] filename="%s-%s-%s.mpg" % (show.ProgramInfo['title'],show.ProgramInfo['episode'],show['recorded']) filename=filename.replace(":",";") filename=filename.replace("\\","-") filename=filename.replace("/","-") filename=filename.replace("..","") filename=filename.replace("!","") filename=filename.replace(">","-") filename=filename.replace("<","-") filename=filename.replace("--","-") filename=filename.replace("--","-") # file save res = wxID_CANCEL try: savedialog = wxFileDialog(self,"Choose a location to save MPEG file","",filename,"*.mpg",style=wxSAVE) res = savedialog.ShowModal() except: print "ERROR: could not open file save dialog" res = wxID_CANCEL replay_device_list['_down%s'%addr]=0 path = filename if res == wxID_CANCEL: replay_device_list['_down%s'%addr]=0 return else: path = filename try: path = savedialog.GetPath() except: path = filename try: savedialog.Destroy() except: print "ERROR: could not destroy save dialog" log("\tDownloading MPEG %s.mpg to file: %s" % (show['recorded'],path)) self.f = MPEGFetcher(addr,show['recorded'],path, self.PanelC.GetItemData(event.m_itemIndex)) self.savempegwin = wxDialog(self, -1, "Saving MPEG File", size=wxSize(350, 200), style=wxCAPTION) win = self.savempegwin sizer = wxBoxSizer(wxVERTICAL) box = wxBoxSizer(wxHORIZONTAL) label = wxStaticText(win, -1, "File:") box.Add(label, 0, wxALIGN_CENTRE|wxALL, 5) text = wxStaticText(win, -1, path) box.Add(text, 1, wxALIGN_CENTRE|wxALL, 5) sizer.AddSizer(box, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5) box = wxBoxSizer(wxHORIZONTAL) label = wxStaticText(win, -1, "Progress:") box.Add(label, 0, wxALIGN_CENTRE|wxALL, 5) self.saving_progresstext = wxStaticText(win, -1, "") box.Add(self.saving_progresstext, 1, wxALIGN_CENTRE|wxALL, 5) sizer.AddSizer(box, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5) box = wxBoxSizer(wxHORIZONTAL) label = wxStaticText(win, -1, "Speed:") box.Add(label, 0, wxALIGN_CENTRE|wxALL, 5) self.saving_progress_speedtext = wxStaticText(win, -1, "") box.Add(self.saving_progress_speedtext, 1, wxALIGN_CENTRE|wxALL, 5) sizer.AddSizer(box, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5) box = wxBoxSizer(wxHORIZONTAL) self.saving_progressbar = wxGauge(win, -1, 1000, wxPoint(110, 50), wxSize(250, 25),style=wxGA_HORIZONTAL|wxGA_SMOOTH) self.saving_progressbar.SetBezelFace(3) self.saving_progressbar.SetShadowWidth(3) self.saving_progressbar.SetValue(1) box.Add(self.saving_progressbar, 1, wxALIGN_CENTRE|wxALL, 5) sizer.AddSizer(box, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxALL, 5) line = wxStaticLine(win, -1, size=(20,-1), style=wxLI_HORIZONTAL) sizer.Add(line, 0, wxGROW|wxALIGN_CENTER_VERTICAL|wxRIGHT|wxTOP, 5) box = wxBoxSizer(wxHORIZONTAL) self.saving_btn = wxButton(win, wxID_CANCEL, " Cancel ") box.Add(self.saving_btn, 0, wxALIGN_CENTRE|wxALL, 5) sizer.AddSizer(box, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5) win.SetSizer(sizer) win.SetAutoLayout(true) sizer.Fit(win) val = win.ShowModal() if val == wxID_CANCEL: try: self.f.running=0 except: print "Hmm, couldn't stop mpeg fetcher thread" def onReplayDeviceSelected(self, event): self.SelectedReplayDeviceID = event.m_itemIndex addr = replay_device_list['_id%s'%self.SelectedReplayDeviceID] info = replay_device_list[addr] self.PanelD.SetPage('Unit: %s
Address: %s
Serial Number: %s'%(info['friendlyName'],addr,info['serialNumber'])) g = self.getGuideData(addr, info['serialNumber']) self.updateGuideDisplay(self.SelectedReplayDeviceID) def updateGuideDisplay(self,DeviceID): if DeviceID != self.SelectedReplayDeviceID: return addr = None try: addr = replay_device_list['_id%s'%DeviceID] except: pass g = None if (addr): try: g = replay_device_list['%s_guide'%addr] except: pass if (g): #f = file("debug\\guide-192.168.1.100.dat","rb") #g = f.read() #f.close() #x = GuideSnapshotHeader(g) #print "Replay Version: %s.%s" % (x['major'],x['minor']) #y = GroupData(g) #self.PanelB.DeleteAllItems() self.PanelC.DeleteAllItems() #sz = ReplayShow().size() count = 0 showindex = {} self.PanelC.itemDataMap = {} else: return guide=None sz=0 try: guide = ReplayGuide(g) except: print "Cannot parse guide" if guide.major == 0: self.PanelD.SetPage('Error: Unknown Replay Guide format
Please send your guide file to replaytv@flyingbuttmonkeys.com') try: sz = guide.showsize except: print "Cannot get showsize" return for show in guide.shows: try: #show = ReplayShow(g, x['showoffset'] + count*sz) self.PanelC.InsertStringItem(count,"%d" % show.ChannelInfo['channel']) self.PanelC.SetStringItem(count,1,"%s (%s)"%(show.ChannelInfo['channellabel'], show.ChannelInfo['channelname'])) self.PanelC.SetStringItem(count,2,show.ProgramInfo['title']) self.PanelC.SetStringItem(count,3,time.asctime(show.ProgramInfo['eventtime'])) self.PanelC.SetStringItem(count,4,"%s minutes" % show.ProgramInfo['minutes']) self.PanelC.SetItemData(count,long(show['recorded'])) self.PanelC.itemDataMap[ long(show['recorded']) ] = ( long(show.ChannelInfo['channel']), str(show.ChannelInfo['channellabel']), str(show.ProgramInfo['title']), long(show['recorded']), long(show.ProgramInfo['minutes']) ) showindex[show['recorded']]=show count = count + 1 except: break self.PanelC.SetColumnWidth(0,wxLIST_AUTOSIZE) self.PanelC.SetColumnWidth(1,wxLIST_AUTOSIZE) self.PanelC.SetColumnWidth(2,wxLIST_AUTOSIZE) self.PanelC.SetColumnWidth(3,wxLIST_AUTOSIZE) self.PanelC.SetColumnWidth(4,wxLIST_AUTOSIZE) #self.PanelC.SetColumnWidth(5,wxLIST_AUTOSIZE) self.showindex = showindex replay_device_list['%s_shows'%addr] = showindex def getGuideData(self, addr, serial=None): f=0 try: f = replay_device_list['%s_fetchingGuide'%addr] except: f = 0 try: g = replay_device_list['%s_guide'%addr] return g except: pass if (f==0): replay_device_list['%s_fetchingGuide'%addr] = 1 gf = GuideFetcher(addr,serial) return None def SaveGuide(self, event): sid = None try: sid = self.SelectedReplayDeviceID except: sid = None if (sid != None): addr = replay_device_list['_id%s'%sid] g = self.getGuideData(addr, None) if (g==None): self.GuideWarnDialog() return filename = "guide-%s.dat"%addr # file save res = wxID_CANCEL try: savedialog = wxFileDialog(self,"Choose a location to save Guide Data file","",filename,"*.dat",style=wxSAVE) res = savedialog.ShowModal() except: print "ERROR: could not open file save dialog" res = wxID_CANCEL replay_device_list['_down%s'%addr]=0 if res == wxID_OK: try: print "writing guide file to %s"%filename f = file(filename,"wb") f.write(g) f.close() except: print "ERROR: couldn't write guide data to %s"%filename return self.GuideWarnDialog() def EnterIP(self, event): win = wxTextEntryDialog(self, "Enter the IP address of a ReplayTV", "Enter the IP address of a ReplayTV", "", wxOK) res = win.ShowModal() ip = validateIPaddress(win.GetValue().strip()) if ip: print "adding %s"%ip if replay_device_list[ip] == None: updateReplayList(ip, 'http://%s/Device_Descr.xml') def GuideWarnDialog(self): win = wxDialog(self, -1, "Info", size=wxSize(400, 250), style=wxCAPTION) sizer = wxBoxSizer(wxVERTICAL) html = '' html = html + 'To save a guide file, first choose a Replay unit from the list. Once the guide is displayed, you can save it as a file.' html = html + '' label = wxHtmlWindow(win,-1,wxPyDefaultPosition, wxSize(400,250)) label.SetPage(html) sizer.Add(label, 0, wxALIGN_CENTRE|wxALL, 5) box = wxBoxSizer(wxHORIZONTAL) btn = wxButton(win, wxID_OK, " Close ") btn.SetDefault() box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5) sizer.AddSizer(box, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5) win.SetSizer(sizer) win.SetAutoLayout(true) sizer.Fit(win) res = win.ShowModal() def CheckNewDialog(self, event): win = wxDialog(self, -1, "Checking for new version", size=wxSize(450, 275), style=wxCAPTION) sizer = wxBoxSizer(wxVERTICAL) html = 'Checking for new version...' label = wxHtmlWindow(win,-1,wxPyDefaultPosition, wxSize(450,275)) label.SetPage(html) sizer.Add(label, 0, wxALIGN_CENTRE|wxALL, 5) box = wxBoxSizer(wxHORIZONTAL) btn = wxButton(win, wxID_OK, " Cancel ") btn.SetDefault() box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5) sizer.AddSizer(box, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5) win.SetSizer(sizer) win.SetAutoLayout(true) sizer.Fit(win) res = win.ShowModal() def AboutDialog(self, event): win = wxDialog(self, -1, "About this Program", size=wxSize(450, 275), style=wxCAPTION) sizer = wxBoxSizer(wxVERTICAL) html = 'ReplayTVClient is a client for 4000 and 5000-series ReplayTV(TM) digital video recorders. It uses Universal Plug and Play (UPnP) to locate Replay units on the local network, downloads, displays and saves the guide, and downloads MPEG video files.

' html = html + '(c) 2003 Michael Rothwell replaytv@flyingbuttmonkeys.com

' html = html + 'When reporting problems, please include a copy of the guide data, the log file, and a description of the problem. A screenshot (jpeg or png format) would also be helpful.

' html = html + 'Written using Python (2.2+) and wxWindows (wxPython).

' html = html + 'Version info: $Id: ReplayTVClient.py,v 1.1 2003/09/04 14:08:09 rothwell Exp $

' html = html + 'For more information and updates, please visit http://www.flyingbuttmonkeys.com/replay/' html = html + '' label = wxHtmlWindow(win,-1,wxPyDefaultPosition, wxSize(450,275)) label.SetPage(html) sizer.Add(label, 0, wxALIGN_CENTRE|wxALL, 5) box = wxBoxSizer(wxHORIZONTAL) btn = wxButton(win, wxID_OK, " Close ") btn.SetDefault() box.Add(btn, 0, wxALIGN_CENTRE|wxALL, 5) sizer.AddSizer(box, 0, wxALIGN_CENTER_VERTICAL|wxALL, 5) win.SetSizer(sizer) win.SetAutoLayout(true) sizer.Fit(win) res = win.ShowModal() def ExitProg(self, event): print "ExitProg" try: p.running=0 self.f.running=0 self.Close(true) except: pass sys.exit(0) # validateIPaddress - looks for dotted-quad ############################################################################## def validateIPaddress(ip): ary = ip.split('.') print ary if len(ary)==4: ok = 1 for i in ary: try: t = int(i) if t>=0 and t<255: ok = 1 else: ok = 0 except: ok = 0 break if ok == 0: ip = None else: ip = None return ip # ReplayTVClient - builds gui. Main wxWindows class, app loop. ############################################################################## class ReplayTVClient(wxApp): def OnInit(self): self.frame = MenuFrame(NULL, -1, "ReplayTV 4xxx and 5xxx Series Client, Version %s" % REVISION) self.frame.Show(true) self.SetTopWindow(self.frame) return true # Logger (to a file) ############################################################################## def log(text): try: if DEBUG==1: logfile.write("%s: %s\n\n"%(time.time(),text)) except: pass ############################################################################## # MAIN ############################################################################## DEBUG = 1 logfile=None try: if DEBUG == 1: logfile=file("replaytvclient.log","w") logfile.write(BUILD) logfile.write(sys.platform) except: logfile=None replay_device_list={} app = ReplayTVClient(0) p = Replay_UPnP() app.MainLoop() p.running=0 try: if DEBUG == 1: logfile.close() except: pass