1 import os, sys, socket, re, urllib, zlib
2 from Cheetah.Template import Template
3 from plugin import Plugin, quote, unquote
4 from urlparse import urlparse
5 from xml.sax.saxutils import escape
6 from lrucache import LRUCache
7 from UserDict import DictMixin
8 from datetime import datetime, timedelta
15 from xml.dom.minidom import parseString
17 from Cheetah.Filters import Filter
19 SCRIPTDIR = os.path.dirname(__file__)
21 CLASS_NAME = 'Youtube'
23 default = "http://gdata.youtube.com/feeds/api/standardfeeds/top_rated"
25 # Preload the templates
26 tcname = os.path.join(SCRIPTDIR, 'templates', 'container.tmpl')
27 ttname = os.path.join(SCRIPTDIR, 'templates', 'TvBus.tmpl')
28 txname = os.path.join(SCRIPTDIR, 'templates', 'container.xsl')
29 CONTAINER_TEMPLATE = file(tcname, 'rb').read()
30 TVBUS_TEMPLATE = file(ttname, 'rb').read()
31 XSL_TEMPLATE = file(txname, 'rb').read()
34 # subprocess is broken for me on windows so super hack
35 def patchSubprocess():
36 o = subprocess.Popen._make_inheritable
38 def _make_inheritable(self, handle):
39 if not handle: return subprocess.GetCurrentProcess()
40 return o(self, handle)
42 subprocess.Popen._make_inheritable = _make_inheritable
44 mswindows = (sys.platform == "win32")
53 os.kill(pid, signal.SIGTERM)
57 handle = ctypes.windll.kernel32.OpenProcess(1, False, pid)
58 ctypes.windll.kernel32.TerminateProcess(handle, -1)
59 ctypes.windll.kernel32.CloseHandle(handle)
62 if path.find("?") == -1:
67 class Youtube(Plugin):
68 CONTENT_TYPE = 'x-container/tivo-videos'
70 videos_feed = "http://gdata.youtube.com/feeds/api/videos"
71 watch_video = "http://www.youtube.com/watch"
72 get_video = 'http://www.youtube.com/get_video'
73 standardfeeds = "http://gdata.youtube.com/feeds/api/standardfeeds/%s"
74 standardfeedsregion = "http://gdata.youtube.com/feeds/api/standardfeeds/%s/%s"
75 user_feed = "http://gdata.youtube.com/feeds/api/users/%s/%s"
76 playlist = "http://gdata.youtube.com/feeds/api/playlists/%s"
82 # from multiprocessing import Process, Lock
83 # p = Process(target=self.LoadVideos)
86 # except(ImportError):
91 #logging.debug('Starting Youtube Plugin Cache')
92 #self.QueryContainer()
95 #def LoadVideos(self):
97 # time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
98 # for k,v in videos.iteritems():
100 def send_file(self, handler, container, name):
101 if handler.headers.getheader('Range') and \
102 handler.headers.getheader('Range') != 'bytes=0-':
103 handler.send_response(206)
104 handler.send_header('Connection', 'close')
105 handler.send_header('Content-Type', 'video/x-tivo-mpeg')
106 handler.send_header('Transfer-Encoding', 'chunked')
107 handler.end_headers()
108 handler.wfile.write("\x30\x0D\x0A")
111 def between(data, From, To):
112 start = data.find(From)
116 end = data.find(To, start)
119 return data[start:end]
121 page = urllib.urlopen(self.watch_video + "?v=%s" % handler.path.split("/")[2]).read()
122 ticket = between(page, '"t": "', '"')
124 handler.send_error(404)
127 url = self.get_video + "?video_id=%s&t=%s" % (handler.path.split("/")[2], ticket)
129 if container.has_key("fmt"):
130 url = url + "&fmt=%s" % (container.get("fmt"))
132 logging.debug('download url is %s' % url)
133 handler.send_response(200)
134 handler.end_headers()
136 settings = '-vcodec mpeg2video -r 29.97 -b 4096K -maxrate 17408k -bufsize 1024k -aspect 4:3 -s 544x480 -ab 192k -ar 48000 -acodec mp2 -ac 2 -f vob -'
138 url = urllib.urlopen(url).geturl()
139 cmd = [config.get('Server', 'ffmpeg'), '-i', url] + settings.split(' ')
140 logging.debug('transcoding using ffmpeg command:')
141 logging.debug(' '.join(cmd))
142 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024), stdout=subprocess.PIPE)
144 cmd = [config.get('Server', 'ffmpeg'), '-i', '-'] + settings.split(' ')
145 source = urllib.urlopen(url)
146 logging.debug('transcoding using ffmpeg command:')
147 logging.debug(' '.join(cmd))
148 ffmpeg = subprocess.Popen(cmd, bufsize=(512 * 1024), stdout=subprocess.PIPE, stdin=source)
151 shutil.copyfileobj(ffmpeg.stdout, handler.wfile)
157 def __duration(self, full_path):
158 return transcode.video_info(full_path)['millisecs']
160 def __est_size(self, full_path, tsn = ''):
161 return int(full_path.duration) * 54672
163 def getVideo(self, entry):
165 for contentlink in entry.getElementsByTagName("media:content"):
166 video['format'] = contentlink.attributes["yt:format"].value
167 video[video['format']] = contentlink.attributes["url"].value
168 video['type'] = contentlink.attributes["type"].value # always application/x-shockwave-flash ?
169 video['medium'] = contentlink.attributes["medium"].value
170 video['duration'] = contentlink.attributes["duration"].value
172 nodes = ['summary', 'id', 'published', 'title', 'updated', 'yt:aboutMe', 'yt:age', 'yt:books', 'yt:company', 'yt:countHint', 'yt:firstName', 'yt:gender', 'yt:hobbies', 'yt:hometown', 'yt:lastName', 'yt:location', 'yt:movies', 'yt:music', 'yt:noembed', 'yt:occupation', 'yt:playlistId', 'yt:playlistTitle', 'yt:position', 'yt:private', 'yt:queryString', 'yt:recorded', 'yt:relationship', 'yt:school', 'yt:uploaded', 'yt:username', 'yt:status', 'yt:videoid', 'media:category', 'media:description', 'media:keywords', 'media:rating', 'media:restriction', 'media:title', 'gml:pos', 'app:edited']
173 for nodename in nodes:
175 node = entry.getElementsByTagName(nodename)[0]
176 if nodename.find(":")!=-1:
177 nodename = nodename.split(":")[1]
178 if node.nodeType == node.ELEMENT_NODE:
179 video[nodename] = node.firstChild.data
180 elif node.nodeType == node.TEXT_NODE:
181 video[nodename] = node.data
182 except(IndexError, AttributeError):
183 if nodename.find(":")!=-1:
184 nodename = nodename.split(":")[1]
185 video[nodename] = False
187 if entry.getElementsByTagName("yt:state"):
188 video['playable'] = False
190 video['playable'] = True
192 if video['playlistId']:
193 video['id'] = video['playlistId']
194 video['group'] = True
195 video['title'] = self.getTitle(entry)
196 url = self.playlist % video['id']
197 feed = parseString(urllib.urlopen(url).read())
198 video['type'] = "playlist"
199 video['isdir'] = True
200 logging.debug('Getting playlist %s at %s' % (video['title'], url))
201 self.videos.update({video['id']: self.getVideos(feed)})
202 video['total_items'] = len(self.videos[video['id']])
203 elif video['videoid']:
204 video['id'] = video['videoid']
205 if entry.getElementsByTagName('gd:rating'):
206 rating = entry.getElementsByTagName("gd:rating")[0]
207 video['rating'] = rating.getAttribute('average')
209 video['rating'] = '3'
210 video['type'] = "video"
211 video['isdir'] = False
212 video['duration'] = entry.getElementsByTagName("yt:duration")[0].attributes["seconds"].value
213 video['vProgramGenre'] = video['keywords'].split(", ")
214 video['channelnumber'] = "0" # maybe have different channels for each youtube channel?
215 video['channelname'] = "YOUTUBE" # maybe have different names for each youtube channel?
216 video['showingBits'] = "1" # change this for different youtube ratings
217 video['displayMinorNumber'] = "111"
218 video['episodeTitle'] = video['title']
219 video['seriesTitle'] = video['title']
220 video['isEpisode'] = 'false'
221 video['vChoreographer'] = []
222 #author = entry.getElementsByTagName("author")[0]
223 #video['author'] = author.getElementsByTagName("name")[0].firstChild.data
224 #video['author.uri'] = author.getElementsByTagName("uri")[0].firstChild.data
225 video['author'] = entry.getElementsByTagName("media:credit")[0].firstChild.data
226 video['vActor'] = [video['author']]
227 video['vExecProducer'] = []
228 video['vGuestStar'] = []
229 video['vSeriesGenre'] = []
231 video['vWriter'] = []
232 video['vProducer'] = []
233 video['showType'] = ('SERIES', '5')
234 video['tvRating'] = "5"
236 video['starRating'] = str(round(float(video['rating']) * 7 / 5)).split(".")[0]
238 video['starRating'] = '4'
239 video['vDirector'] = []
240 video['mpaaRating'] = "N8"
241 video['episodeNumber'] = '1'
242 video['seriesId'] = ""
244 duration_delta = timedelta(milliseconds=int(video['duration'])*1000)
247 from xml.utils import iso8601
248 date = iso8601.parse(text(video['uploaded']))
249 utcdate = datetime.utcfromtimestamp(date)
250 video['time'] = date.isoformat()
251 video['startTime'] = date.isoformat()
252 video['stopTime'] = date+duration
254 now = datetime.utcnow()
255 video['time'] = now.isoformat()
256 video['startTime'] = now.isoformat()
257 video['stopTime'] = (now + duration_delta).isoformat()
259 #video['originalAirDate'] = video['uploaded'].replace(".000Z", "")
260 video['movieYear'] = video['uploaded'].split("T")[0].split("-")[0]
262 min = duration_delta.seconds / 60
263 sec = duration_delta.seconds % 60
266 video['iso_duration'] = 'P' + str(duration_delta.days) + \
267 'DT' + str(hours) + 'H' + str(min) + \
269 video['size'] = int(video['duration']) * 546720
270 video['milliseconds'] = int(video['duration']) * 1000
271 elif video['username']:
272 folder['group'] = True
273 url = entry.getElementsByTagName("content")[0].getAttribute('src')
274 feed = parseString(urllib.urlopen(url).read())
275 video['title'] = self.getTitle(feed)
276 video['id'] = video['username']
277 video['type'] = "subscription"
278 video['isdir'] = True
279 self.videos[video['id']] = self.getVideos(feed)
280 video['total_items'] = len(self.videos[video['id']])
281 elif entry.getElementsByTagName("media:player"):
282 video['id'] = entry.getElementsByTagName("media:player")[0].getAttribute('url').replace('http://www.youtube.com/watch?v=', '')
283 rating = entry.getElementsByTagName("gd:rating")[0]
284 video['rating'] = rating.getAttribute('average')
285 video['type'] = "video"
286 video['isdir'] = False
287 video['duration'] = entry.getElementsByTagName("yt:duration")[0].attributes["seconds"].value
288 video['vProgramGenre'] = video['keywords'].split(", ")
289 video['channelnumber'] = "0" # maybe have different channels for each youtube channel?
290 video['channelname'] = "YOUTUBE" # maybe have different names for each youtube channel?
291 video['showingBits'] = "1" # change this for different youtube ratings
292 video['displayMinorNumber'] = "111"
293 video['episodeTitle'] = video['title']
294 video['seriesTitle'] = video['title']
295 video['isEpisode'] = 'false'
296 video['vChoreographer'] = []
297 author = entry.getElementsByTagName("author")[0]
298 video['author'] = author.getElementsByTagName("name")[0].firstChild.data
299 #video['author.uri'] = author.getElementsByTagName("uri")[0].firstChild.data
300 video['vActor'] = [video['author']]
301 video['vExecProducer'] = []
302 video['vGuestStar'] = []
303 video['vSeriesGenre'] = []
305 video['vWriter'] = []
306 video['vProducer'] = []
307 video['showType'] = ('SERIES', '5')
308 video['tvRating'] = "5"
310 video['starRating'] = str(round(float(video['rating']) * 7 / 5)).split(".")[0]
312 video['starRating'] = '4'
313 video['vDirector'] = []
314 video['mpaaRating'] = "N8"
315 video['episodeNumber'] = '1'
316 video['seriesId'] = ""
318 duration_delta = timedelta(milliseconds=int(video['duration'])*1000)
321 from xml.utils import iso8601
322 date = iso8601.parse(text(video['uploaded']))
323 utcdate = datetime.utcfromtimestamp(date)
324 video['time'] = date.isoformat()
325 video['startTime'] = date.isoformat()
326 video['stopTime'] = date+duration
328 now = datetime.utcnow()
329 video['time'] = now.isoformat()
330 video['startTime'] = now.isoformat()
331 video['stopTime'] = (now + duration_delta).isoformat()
333 #video['originalAirDate'] = video['uploaded'].replace(".000Z", "")
334 video['movieYear'] = video['updated'].split("T")[0].split("-")[0]
336 min = duration_delta.seconds / 60
337 sec = duration_delta.seconds % 60
340 video['iso_duration'] = 'P' + str(duration_delta.days) + \
341 'DT' + str(hours) + 'H' + str(min) + \
343 video['size'] = int(video['duration']) * 546720
344 video['milliseconds'] = int(video['duration']) * 1000
346 logging.debug('The entry named %s probably does not have proper pyTiVo Youtube Plugin support' % video['title'])
351 def getVideos(self, feed):
353 for entry in feed.getElementsByTagName("entry"):
354 video = self.getVideo(entry)
355 if video: videos.append(video)
358 def getTitle(self, feed):
359 return feed.getElementsByTagName("title")[0].firstChild.data
361 def item_count(self, query, files, last_start=0):
362 """Return only the desired portion of the list, as specified by
363 ItemCount, AnchorItem and AnchorOffset. 'files' is either a
364 list of objects with an 'id' attribute.
366 totalFiles = len(files)
369 if totalFiles and query.has_key('ItemCount'):
370 count = int(query['ItemCount'][0])
372 if query.has_key('AnchorItem'):
373 anchor = query['AnchorItem'][0]
374 anchor = unquote(anchor)
375 anchor = anchor.replace("/%s/" % query['Container'][0].split("/")[0], "", 1)
376 anchor = anchor.replace("/TiVoConnect?Command=QueryContainer&Container=%s&id=" % query['Container'][0],'',1)
377 anchor = anchor.replace("/TiVoConnect?Command=QueryContainer&Container=%s/" % query['Container'][0],'',1)
379 filenames = [x['id'] for x in files]
382 index = filenames.index(anchor)
384 logging.debug('Anchor not found: %s' % anchor)
389 if query.has_key('AnchorOffset'):
390 index += int(query['AnchorOffset'][0])
394 files = files[index:index + count]
397 if index + count < 0:
399 files = files[index + count:index]
402 else: # No AnchorItem
405 files = files[:count]
407 index = count % len(files)
408 files = files[count:]
410 return files, totalFiles, index
412 def QueryContainer(self, handler, query):
413 tsn = handler.headers.getheader('tsn', '')
414 subcname = query['Container'][0]
415 cname = subcname.split('/')[0]
416 container = handler.server.containers[cname]
423 if subcname.find("/")!=-1:
424 id = id.split("/")[1]
426 videos = self.videos[id]
428 path = self.playlist % id
429 logging.debug('Getting: %s' % path)
430 feed = parseString(urllib.urlopen(path).read())
431 self.videos.update({id: self.getVideos(feed)})
432 videos = self.videos[id]
435 videos = self.videos[id]
438 if container.has_key('user') and not container.has_key('path') and not container.has_key('playlist'):
439 userfolders = ["uploads", "favorites", "playlists"]
441 for folder in userfolders:
443 path = "http://gdata.youtube.com/feeds/api/users/%s/%s" % (container.get('user'), folder)
445 feed = parseString(urllib.urlopen(path).read())
448 self.videos.update({"%s/%s" % (subcname,folder): self.getVideos(feed)})
449 video['total_items'] = len(self.videos["%s/%s" % (subcname,folder)])
450 video['group'] = False
451 video['title'] = folder
452 video['id'] = "%s/%s" % (subcname,folder)
453 video['isdir'] = True
456 if container.has_key('path'):
457 if container.get('path').startswith("http://gdata.youtube.com/feeds/"):
458 path = container.get('path')
459 elif container.has_key("author"):
460 path = self.user_feed % (container.get('author'),container.get('path'))
461 elif container.has_key("user"):
462 path = self.user_feed % (container.get('user'),container.get('path'))
464 path = container.get('path').replace(" ", "_")
466 if container.has_key("category"):
467 path = "%s_%s" % (path, container.get("category"))
468 if container.has_key("region"):
469 self.standardfeedsregion % (container.get("region").upper(), path)
471 path = self.standardfeeds % path
472 elif container.has_key('playlist'):
473 path = self.playlist % container.get('playlist')
475 if container.has_key('search'):
476 path = path + "%sq=%s" % (qoramp(path), container.get('search'))
477 if path.split("?")[0] == "":
478 path = self.videos_feed + path
479 if container.has_key('q'):
480 path = path + "%sq=%s" % (qoramp(path), container.get('q'))
481 if container.has_key('start-index'):
482 path = path + "%sstart-index=%s" % (qoramp(path), container.get('start-index'))
483 if container.has_key('max'):
484 path = path + "%smax-results=%s" % (qoramp(path), container.get('max'))
485 elif container.has_key('max-results'):
486 path = path + "%smax-results=%s" % (qoramp(path), container.get('max-results'))
488 path = path + "%smax-results=%s" % (qoramp(path), '50')
489 if container.has_key('safeSearch'):
490 path = path + "%ssafeSearch=%s" % (qoramp(path), container.get('safeSearch'))
492 path = path + "%ssafeSearch=%s" % (qoramp(path), "strict")
493 if container.has_key("time"):
494 path = path + "%stime=%s" % (qoramp(path), container.get('time'))
495 if container.has_key("uploader"):
496 path = path + "%suploader=%s" % (qoramp(path), container.get('uploader'))
497 if container.has_key("restriction"):
498 path = path + "%srestriction=%s" % (qoramp(path), container.get('restriction'))
499 if container.has_key("orderby"):
500 path = path + "%sorderby=%s" % (qoramp(path), container.get('orderby'))
501 if container.has_key("lr"):
502 path = path + "%slr=%s" % (qoramp(path), container.get('lr'))
503 if container.has_key("location-radius"):
504 path = path + "%slocation-radius=%s" % (qoramp(path), container.get('location-radius'))
505 if container.has_key("location"):
506 path = path + "%slocation=%s" % (qoramp(path), container.get('location'))
507 if container.has_key("format"):
508 path = path + "%sformat=%s" % (qoramp(path), container.get('format'))
509 if container.has_key("client"):
510 path = path + "%sclient=%s" % (qoramp(path), container.get('client'))
511 if container.has_key("category"):
512 path = path + "%scategory=%s" % (qoramp(path), container.get('category'))
513 if container.has_key('v'):
514 path = path + "%sv=%s" % (qoramp(path), container.get('v'))
515 elif container.has_key('version'):
516 path = path + "%sv=%s" % (qoramp(path), container.get('version'))
518 path = path + "%sv=2" % (qoramp(path))
520 logging.debug('Getting: %s' % path)
521 feed = parseString(urllib.urlopen(path).read())
523 self.videos.update({id: self.getVideos(feed)})
524 videos = self.videos[id]
526 if container.has_key("refresh"):
527 if container.get("refresh").lower() == "yes":
529 refresh['total_items'] = 1
530 refresh['group'] = False
531 refresh['title'] = "Refresh"
532 refresh['id'] = "refresh"
533 refresh['isdir'] = True
534 videos.append(refresh)
536 if container.has_key("group"):
540 if video[container.get("group")] in groups:
541 self.videos["%s/%s" % (subcname,video[container.get("group")])].append(video)
542 for folder in folders:
543 if folder['id'] == "%s/%s" % (subcname,video[container.get("group")]):
544 folder['total_items'] = folder['total_items'] + 1
546 self.videos["%s/%s" % (subcname,video[container.get("group")])] = []
547 self.videos["%s/%s" % (subcname,video[container.get("group")])].append(video)
548 groups.append(video[container.get("group")])
550 folder['total_items'] = 1
551 folder['group'] = False
552 folder['title'] = video[container.get("group")]
553 folder['id'] = "%s/%s" % (subcname,video[container.get("group")])
554 folder['isdir'] = True
555 folders.append(folder)
556 for folder in folders:
557 if folder['total_items'] == 1:
558 folder.update(self.videos[folder['id']][0])
559 videos, total, start = self.item_count(query, folders)
561 videos, total, start = self.item_count(query, videos)
563 handler.send_response(200)
564 handler.end_headers()
565 t = Template(CONTAINER_TEMPLATE, filter=EncodeUnicode)
567 t.name = subcname #self.getTitle(feed)
574 t.guid = config.getGUID()
575 t.tivos = handler.tivos
576 t.tivo_names = handler.tivo_names
577 handler.wfile.write(t)
579 def TVBusQuery(self, handler, query):
580 tsn = handler.headers.getheader('tsn', '')
582 search = "http://gdata.youtube.com/feeds/api/videos?q=%s&max-results=%s&v=2"
586 for video in self.videos[query['Container'][0]]:
587 if video['id'] == id:
592 file_info = self.getVideo(parseString(urllib.urlopen(search % (id,"1")).read()))
594 file_info = self.getVideo(parseString(urllib.urlopen(search % (id,"1")).read()))
596 file_info = self.getVideo(parseString(urllib.urlopen(search % (id,"1")).read()))
598 handler.send_response(200)
599 handler.end_headers()
600 t = Template(TVBUS_TEMPLATE, filter=EncodeUnicode)
603 handler.wfile.write(t)
605 def XSL(self, handler, query):
606 handler.send_response(200)
607 handler.end_headers()
608 handler.wfile.write(XSL_TEMPLATE)
610 def Push(self, handler, query):
611 id = unquote(query['id'][0])
613 search = "http://gdata.youtube.com/feeds/api/videos?q=%s&max-results=%s&v=2"
615 tsn = query['tsn'][0]
616 for key in handler.tivo_names:
617 if handler.tivo_names[key] == tsn:
623 for video in self.videos[query['Container'][0]]:
624 if video['id'] == id:
629 file_info = self.getVideo(parseString(urllib.urlopen(search % (id,"1")).read()))
631 file_info = self.getVideo(parseString(urllib.urlopen(search % (id,"1")).read()))
634 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
635 s.connect(('tivo.com',123))
636 ip = s.getsockname()[0]
637 container = quote(query['Container'][0].split('/')[0])
638 port = config.getPort()
640 url = 'http://%s:%s/%s/%s' % (ip, port, container, quote(id))
647 description = file_info['description'],
648 duration = int(file_info['duration']) / 1000,
649 size = file_info['size'],
650 title = file_info['title'],
651 subtitle = file_info['title'])
654 handler.send_response(500)
655 handler.end_headers()
656 handler.wfile.write('%s\n\n%s' % (e, traceback.format_exc() ))
659 referer = handler.headers.getheader('Referer')
660 handler.send_response(302)
661 handler.send_header('Location', referer)
662 handler.end_headers()
664 class EncodeUnicode(Filter):
665 def filter(self, val, **kw):
666 """Encode Unicode strings, by default in UTF-8"""
668 if kw.has_key('encoding'):
669 encoding = kw['encoding']
673 if type(val) == type(u''):
674 filtered = val.encode(encoding)
679 class VideoDetails(DictMixin):
681 def __init__(self, d=None):
687 def __getitem__(self, key):
688 if key not in self.d:
689 self.d[key] = self.default(key)
692 def __contains__(self, key):
695 def __setitem__(self, key, value):
698 def __delitem__(self):
705 return self.d.__iter__()
708 return self.d.iteritems()
710 def default(self, key):
713 'episodeNumber' : '0',
714 'displayMajorNumber' : '0',
715 'displayMinorNumber' : '0',
716 'isEpisode' : 'true',
717 'colorCode' : ('COLOR', '4'),
718 'showType' : ('SERIES', '5'),
719 'tvRating' : ('NR', '7')
723 elif key.startswith('v'):