9 from urllib
import quote
, unquote
10 from xml
.dom
import minidom
11 from xml
.sax
.saxutils
import escape
13 from Cheetah
.Template
import Template
17 from plugin
import EncodeUnicode
, Plugin
19 logger
= logging
.getLogger('pyTivo.togo')
20 tag_data
= metadata
.tag_data
22 SCRIPTDIR
= os
.path
.dirname(__file__
)
26 # Some error/status message templates
28 MISSING
= """<h3>Missing Data</h3> <p>You must set both "tivo_mak" and
29 "togo_path" before using this function.</p>"""
31 TRANS_QUEUE
= """<h3>Queued for Transfer</h3> <p>%s</p> <p>queued for
32 transfer to:</p> <p>%s</p>"""
34 TRANS_STOP
= """<h3>Transfer Stopped</h3> <p>Your transfer of:</p>
35 <p>%s</p> <p>has been stopped.</p>"""
37 UNQUEUE
= """<h3>Removed from Queue</h3> <p>%s</p> <p>has been removed
38 from the queue.</p>"""
40 UNABLE
= """<h3>Unable to Connect to TiVo</h3> <p>pyTivo was unable to
41 connect to the TiVo at %s.</p> <p>This is most likely caused by an
42 incorrect Media Access Key. Please return to the Web Configuration page
43 and double check your <b>tivo_mak</b> setting.</p>"""
45 # Preload the templates
46 tnname
= os
.path
.join(SCRIPTDIR
, 'templates', 'npl.tmpl')
47 NPL_TEMPLATE
= file(tnname
, 'rb').read()
49 status
= {} # Global variable to control download threads
50 tivo_cache
= {} # Cache of TiVo NPL
51 queue
= {} # Recordings to download -- list per TiVo
52 basic_meta
= {} # Data from NPL, parsed, indexed by progam URL
54 auth_handler
= urllib2
.HTTPDigestAuthHandler()
55 cj
= cookielib
.LWPCookieJar()
56 tivo_opener
= urllib2
.build_opener(urllib2
.HTTPCookieProcessor(cj
),
60 CONTENT_TYPE
= 'text/html'
62 def tivo_open(self
, url
):
63 # Loop just in case we get a server busy message
66 # Open the URL using our authentication/cookie opener
67 return tivo_opener
.open(url
)
69 # Do a retry if the TiVo responds that the server is busy
70 except urllib2
.HTTPError
, e
:
75 # Throw the error otherwise
78 def NPL(self
, handler
, query
):
80 shows_per_page
= 50 # Change this to alter the number of shows returned
82 has_tivodecode
= bool(config
.get_bin('tivodecode'))
85 tivoIP
= query
['TiVo'][0]
86 tsn
= config
.tivos_by_ip(tivoIP
)
87 tivo_name
= config
.tivo_names
[tsn
]
88 tivo_mak
= config
.get_tsn('tivo_mak', tsn
)
89 theurl
= ('https://' + tivoIP
+
90 '/TiVoConnect?Command=QueryContainer&ItemCount=' +
91 str(shows_per_page
) + '&Container=/NowPlaying')
93 folder
+= query
['Folder'][0]
94 theurl
+= '/' + folder
95 if 'AnchorItem' in query
:
96 theurl
+= '&AnchorItem=' + quote(query
['AnchorItem'][0])
97 if 'AnchorOffset' in query
:
98 theurl
+= '&AnchorOffset=' + query
['AnchorOffset'][0]
100 if (theurl
not in tivo_cache
or
101 (time
.time() - tivo_cache
[theurl
]['thepage_time']) >= 60):
102 # if page is not cached or old then retreive it
103 auth_handler
.add_password('TiVo DVR', tivoIP
, 'tivo', tivo_mak
)
105 page
= self
.tivo_open(theurl
)
107 handler
.redir(UNABLE
% tivoIP
, 10)
109 tivo_cache
[theurl
] = {'thepage': minidom
.parse(page
),
110 'thepage_time': time
.time()}
113 xmldoc
= tivo_cache
[theurl
]['thepage']
114 items
= xmldoc
.getElementsByTagName('Item')
115 TotalItems
= tag_data(xmldoc
, 'Details/TotalItems')
116 ItemStart
= tag_data(xmldoc
, 'ItemStart')
117 ItemCount
= tag_data(xmldoc
, 'ItemCount')
118 FirstAnchor
= tag_data(items
[0], 'Links/Content/Url')
123 entry
['ContentType'] = tag_data(item
, 'ContentType')
124 for tag
in ('CopyProtected', 'UniqueId'):
125 value
= tag_data(item
, tag
)
128 if entry
['ContentType'] == 'x-tivo-container/folder':
129 entry
['Title'] = tag_data(item
, 'Title')
130 entry
['TotalItems'] = tag_data(item
, 'TotalItems')
131 lc
= tag_data(item
, 'LastCaptureDate')
133 lc
= tag_data(item
, 'LastChangeDate')
134 entry
['LastChangeDate'] = time
.strftime('%b %d, %Y',
135 time
.localtime(int(lc
, 16)))
137 keys
= {'Icon': 'Links/CustomIcon/Url',
138 'Url': 'Links/Content/Url',
139 'SourceSize': 'Details/SourceSize',
140 'Duration': 'Details/Duration',
141 'CaptureDate': 'Details/CaptureDate'}
143 value
= tag_data(item
, keys
[key
])
147 entry
['SourceSize'] = ( '%.3f GB' %
148 (float(entry
['SourceSize']) / (1024 ** 3)) )
150 dur
= int(entry
['Duration']) / 1000
151 entry
['Duration'] = ( '%02d:%02d:%02d' %
152 (dur
/ 3600, (dur
% 3600) / 60, dur
% 60) )
154 entry
['CaptureDate'] = time
.strftime('%b %d, %Y',
155 time
.localtime(int(entry
['CaptureDate'], 16)))
158 if url
in basic_meta
:
159 entry
.update(basic_meta
[url
])
161 basic_data
= metadata
.from_container(item
)
162 entry
.update(basic_data
)
163 basic_meta
[url
] = basic_data
174 t
= Template(NPL_TEMPLATE
, filter=EncodeUnicode
)
180 t
.queue
= queue
[tivoIP
]
181 t
.has_tivodecode
= has_tivodecode
184 t
.container
= handler
.cname
187 t
.TotalItems
= int(TotalItems
)
188 t
.ItemStart
= int(ItemStart
)
189 t
.ItemCount
= int(ItemCount
)
190 t
.FirstAnchor
= quote(FirstAnchor
)
191 t
.shows_per_page
= shows_per_page
192 handler
.send_html(str(t
), refresh
='300')
194 def get_tivo_file(self
, tivoIP
, url
, mak
, togo_path
):
196 status
[url
].update({'running': True, 'queued': False})
198 parse_url
= urlparse
.urlparse(url
)
200 name
= unquote(parse_url
[2])[10:].split('.')
201 id = unquote(parse_url
[4]).split('id=')[1]
202 name
.insert(-1, ' - ' + id + '.')
203 if status
[url
]['decode']:
205 outfile
= os
.path
.join(togo_path
, ''.join(name
))
207 if status
[url
]['save']:
208 meta
= basic_meta
[url
]
209 details_url
= 'https://%s/TiVoVideoDetails?id=%s' % (tivoIP
, id)
211 handle
= self
.tivo_open(details_url
)
212 meta
.update(metadata
.from_details(handle
))
216 metafile
= open(outfile
+ '.txt', 'w')
217 metadata
.dump(metafile
, meta
)
220 auth_handler
.add_password('TiVo DVR', url
, 'tivo', mak
)
222 handle
= self
.tivo_open(url
)
223 except urllib2
.HTTPError
, e
:
224 status
[url
]['running'] = False
225 status
[url
]['error'] = e
.code
228 except urllib2
.URLError
, e
:
229 status
[url
]['running'] = False
230 status
[url
]['error'] = e
.reason
231 logger
.error(e
.reason
)
234 tivo_name
= config
.tivo_names
[config
.tivos_by_ip(tivoIP
)]
236 logger
.info('[%s] Start getting "%s" from %s' %
237 (time
.strftime('%d/%b/%Y %H:%M:%S'), outfile
, tivo_name
))
239 if status
[url
]['decode']:
240 tivodecode_path
= config
.get_bin('tivodecode')
241 tcmd
= [tivodecode_path
, '-m', mak
, '-o', outfile
, '-']
242 tivodecode
= subprocess
.Popen(tcmd
, stdin
=subprocess
.PIPE
,
243 bufsize
=(512 * 1024))
246 f
= open(outfile
, 'wb')
248 start_time
= time
.time()
249 last_interval
= start_time
252 while status
[url
]['running']:
253 output
= handle
.read(1024000)
256 length
+= len(output
)
259 elapsed
= now
- last_interval
261 status
[url
]['rate'] = '%.2f Mb/s' % (length
* 8.0 /
262 (elapsed
* 1024 * 1024))
263 status
[url
]['size'] += length
266 if status
[url
]['running']:
267 status
[url
]['finished'] = True
268 except Exception, msg
:
269 status
[url
]['running'] = False
273 status
[url
]['size'] += length
274 if status
[url
]['running']:
275 mega_elapsed
= (now
- start_time
) * 1024 * 1024
278 size
= status
[url
]['size']
279 rate
= size
* 8.0 / mega_elapsed
280 logger
.info('[%s] Done getting "%s" from %s, %d bytes, %.2f Mb/s' %
281 (time
.strftime('%d/%b/%Y %H:%M:%S'), outfile
,
282 tivo_name
, size
, rate
))
283 status
[url
]['running'] = False
286 if status
[url
]['save']:
287 os
.remove(outfile
+ '.txt')
288 logger
.info('[%s] Transfer of "%s" from %s aborted' %
289 (time
.strftime('%d/%b/%Y %H:%M:%S'), outfile
,
293 def process_queue(self
, tivoIP
, mak
, togo_path
):
296 url
= queue
[tivoIP
][0]
297 self
.get_tivo_file(tivoIP
, url
, mak
, togo_path
)
301 def ToGo(self
, handler
, query
):
302 togo_path
= config
.get_server('togo_path')
303 for name
, data
in config
.getShares():
304 if togo_path
== name
:
305 togo_path
= data
.get('path')
307 tivoIP
= query
['TiVo'][0]
308 tsn
= config
.tivos_by_ip(tivoIP
)
309 tivo_mak
= config
.get_tsn('tivo_mak', tsn
)
310 urls
= query
.get('Url', [])
311 decode
= 'decode' in query
312 save
= 'save' in query
314 status
[theurl
] = {'running': False, 'error': '', 'rate': '',
315 'queued': True, 'size': 0, 'finished': False,
316 'decode': decode
, 'save': save
}
318 queue
[tivoIP
].append(theurl
)
320 queue
[tivoIP
] = [theurl
]
321 thread
.start_new_thread(ToGo
.process_queue
,
322 (self
, tivoIP
, tivo_mak
, togo_path
))
323 logger
.info('[%s] Queued "%s" for transfer to %s' %
324 (time
.strftime('%d/%b/%Y %H:%M:%S'),
325 unquote(theurl
), togo_path
))
326 urlstring
= '<br>'.join([unquote(x
) for x
in urls
])
327 message
= TRANS_QUEUE
% (urlstring
, togo_path
)
330 handler
.redir(message
, 5)
332 def ToGoStop(self
, handler
, query
):
333 theurl
= query
['Url'][0]
334 status
[theurl
]['running'] = False
335 handler
.redir(TRANS_STOP
% unquote(theurl
))
337 def Unqueue(self
, handler
, query
):
338 theurl
= query
['Url'][0]
339 tivoIP
= query
['TiVo'][0]
341 queue
[tivoIP
].remove(theurl
)
342 logger
.info('[%s] Removed "%s" from queue' %
343 (time
.strftime('%d/%b/%Y %H:%M:%S'),
345 handler
.redir(UNQUEUE
% unquote(theurl
))