From 6f0816373f5a0f462c9c3d9a4208e386a41c2358 Mon Sep 17 00:00:00 2001 From: Jason Michalski Date: Sun, 25 Nov 2007 01:12:27 -0600 Subject: [PATCH] Changed line endings --- Config.py | 202 +++++++------- plugin.py | 258 +++++++++--------- plugins/music/music.py | 176 ++++++------- plugins/video/transcode.py | 636 ++++++++++++++++++++++----------------------- plugins/video/video.py | 590 ++++++++++++++++++++--------------------- pyTivo.conf | 102 ++++---- pyTivoConfigurator.py | 240 ++++++++--------- 7 files changed, 1102 insertions(+), 1102 deletions(-) diff --git a/Config.py b/Config.py index 646800c..b0d729b 100644 --- a/Config.py +++ b/Config.py @@ -1,101 +1,101 @@ -import ConfigParser, os -import re -from ConfigParser import NoOptionError - -BLACKLIST_169 = ('540', '649') - -config = ConfigParser.ConfigParser() -p = os.path.dirname(__file__) -config.read(os.path.join(p, 'pyTivo.conf')) - -def get169Setting(tsn): - if not tsn: - return True - - if config.has_section('_tivo_' + tsn): - if config.has_option('_tivo_' + tsn, 'aspect169'): - if config.get('_tivo_' + tsn, 'aspect169').lower() == 'true': - return True - else: - return False - - if tsn[:3] in BLACKLIST_169: - return False - - return True - -def getShares(): - return filter( lambda x: not(x.startswith('_tivo_') or x == 'Server'), config.sections()) - -def getDebug(): - try: - debug = config.get('Server', 'debug') - if debug.lower() == 'true': - return True - else: - return False - except NoOptionError: - return False - -def getHack83(): - try: - debug = config.get('Server', 'hack83') - if debug.lower() == 'true': - return True - else: - return False - except NoOptionError: - return True - -def get(section, key): - return config.get(section, key) - -def getValidWidths(): - return [1440, 720, 704, 544, 480, 352] - -def getValidHeights(): - return [720, 480] # Technically 240 is also supported - -# Return the number in list that is nearest to x -# if two values are equidistant, return the larger -def nearest(x, list): - return reduce(lambda a, b: closest(x,a,b), list) - -def closest(x,a, b): - if abs(x-a) < abs(x-b) or (abs(x-a) == abs(x-b)and a>b): - return a - else: - return b - - -def nearestTivoWidth(width): - return nearest(width, getValidWidths()) - -def getTivoHeight(): - try: - height = int(config.get('Server', 'height')) - print nearest(height, getValidHeights()) - return nearest(height, getValidHeights()) - except NoOptionError: #default - return 480 - -def getTivoWidth(): - try: - width = int(config.get('Server', 'width')) - print nearestTivoWidth(width) - return nearestTivoWidth(width) - except NoOptionError: #default - return 544 - -def getAudioBR(): - try: - return config.get('Server', 'audio_br') - except NoOptionError: #default to 192 - return '192K' - -def getVideoBR(): - try: - return config.get('Server', 'video_br') - except NoOptionError: #default to 4096K - return '4096K' - +import ConfigParser, os +import re +from ConfigParser import NoOptionError + +BLACKLIST_169 = ('540', '649') + +config = ConfigParser.ConfigParser() +p = os.path.dirname(__file__) +config.read(os.path.join(p, 'pyTivo.conf')) + +def get169Setting(tsn): + if not tsn: + return True + + if config.has_section('_tivo_' + tsn): + if config.has_option('_tivo_' + tsn, 'aspect169'): + if config.get('_tivo_' + tsn, 'aspect169').lower() == 'true': + return True + else: + return False + + if tsn[:3] in BLACKLIST_169: + return False + + return True + +def getShares(): + return filter( lambda x: not(x.startswith('_tivo_') or x == 'Server'), config.sections()) + +def getDebug(): + try: + debug = config.get('Server', 'debug') + if debug.lower() == 'true': + return True + else: + return False + except NoOptionError: + return False + +def getHack83(): + try: + debug = config.get('Server', 'hack83') + if debug.lower() == 'true': + return True + else: + return False + except NoOptionError: + return True + +def get(section, key): + return config.get(section, key) + +def getValidWidths(): + return [1440, 720, 704, 544, 480, 352] + +def getValidHeights(): + return [720, 480] # Technically 240 is also supported + +# Return the number in list that is nearest to x +# if two values are equidistant, return the larger +def nearest(x, list): + return reduce(lambda a, b: closest(x,a,b), list) + +def closest(x,a, b): + if abs(x-a) < abs(x-b) or (abs(x-a) == abs(x-b)and a>b): + return a + else: + return b + + +def nearestTivoWidth(width): + return nearest(width, getValidWidths()) + +def getTivoHeight(): + try: + height = int(config.get('Server', 'height')) + print nearest(height, getValidHeights()) + return nearest(height, getValidHeights()) + except NoOptionError: #default + return 480 + +def getTivoWidth(): + try: + width = int(config.get('Server', 'width')) + print nearestTivoWidth(width) + return nearestTivoWidth(width) + except NoOptionError: #default + return 544 + +def getAudioBR(): + try: + return config.get('Server', 'audio_br') + except NoOptionError: #default to 192 + return '192K' + +def getVideoBR(): + try: + return config.get('Server', 'video_br') + except NoOptionError: #default to 4096K + return '4096K' + diff --git a/plugin.py b/plugin.py index b9958d2..a26d46c 100644 --- a/plugin.py +++ b/plugin.py @@ -1,129 +1,129 @@ -import os, shutil, re -from urllib import unquote, unquote_plus -from urlparse import urlparse - -def GetPlugin(name): - module_name = '.'.join(['plugins', name, name]) - module = __import__(module_name, globals(), locals(), name) - plugin = getattr(module, name)() - return plugin - -class Plugin(object): - - def __new__(cls, *args, **kwds): - it = cls.__dict__.get('__it__') - if it is not None: - return it - cls.__it__ = it = object.__new__(cls) - it.init(*args, **kwds) - return it - - def init(self): - pass - - content_type = '' - - def SendFile(self, handler, container, name): - o = urlparse("http://fake.host" + handler.path) - path = unquote_plus(o[2]) - handler.send_response(200) - handler.end_headers() - f = file(container['path'] + path[len(name)+1:], 'rb') - shutil.copyfileobj(f, handler.wfile) - - def get_local_path(self, handler, query): - - subcname = query['Container'][0] - container = handler.server.containers[subcname.split('/')[0]] - - path = container['path'] - for folder in subcname.split('/')[1:]: - if folder == '..': - return False - path = os.path.join(path, folder) - return path - - def get_files(self, handler, query, filterFunction=None): - subcname = query['Container'][0] - path = self.get_local_path(handler, query) - - files = os.listdir(path) - if filterFunction: - files = filter(filterFunction, files) - totalFiles = len(files) - - def dir_sort(x, y): - xdir = os.path.isdir(os.path.join(path, x)) - ydir = os.path.isdir(os.path.join(path, y)) - - if xdir and ydir: - return name_sort(x, y) - elif xdir: - return -1 - elif ydir: - return 1 - else: - return name_sort(x, y) - - def name_sort(x, y): - numbername = re.compile(r'(\d*)(.*)') - m = numbername.match(x) - xNumber = m.group(1) - xStr = m.group(2) - m = numbername.match(y) - yNumber = m.group(1) - yStr = m.group(2) - - if xNumber and yNumber: - xNumber, yNumber = int(xNumber), int(yNumber) - if xNumber == yNumber: - return cmp(xStr, yStr) - else: - return cmp(xNumber, yNumber) - elif xNumber: - return -1 - elif yNumber: - return 1 - else: - return cmp(xStr, yStr) - - files.sort(dir_sort) - - index = 0 - count = 10 - if query.has_key('ItemCount'): - count = int(query['ItemCount'] [0]) - - if query.has_key('AnchorItem'): - anchor = unquote(query['AnchorItem'][0]) - for i in range(len(files)): - if os.path.isdir(os.path.join(path,files[i])): - file_url = '/TiVoConnect?Command=QueryContainer&Container=' + subcname + '/' + files[i] - else: - file_url = '/' + subcname + '/' + files[i] - if file_url == anchor: - if count > 0: - index = i + 1 - elif count < 0: - index = i - 1 - else: - index = i - break - if query.has_key('AnchorOffset'): - index = index + int(query['AnchorOffset'][0]) - - #foward count - if index < index + count: - files = files[index:index + count ] - return files, totalFiles, index - #backwards count - else: - print 'index, count', index, count - print index + count - #off the start of the list - if index + count < 0: - print 0 - (index + count) - index += 0 - (index + count) - print index + count - files = files[index + count:index] - return files, totalFiles, index + count +import os, shutil, re +from urllib import unquote, unquote_plus +from urlparse import urlparse + +def GetPlugin(name): + module_name = '.'.join(['plugins', name, name]) + module = __import__(module_name, globals(), locals(), name) + plugin = getattr(module, name)() + return plugin + +class Plugin(object): + + def __new__(cls, *args, **kwds): + it = cls.__dict__.get('__it__') + if it is not None: + return it + cls.__it__ = it = object.__new__(cls) + it.init(*args, **kwds) + return it + + def init(self): + pass + + content_type = '' + + def SendFile(self, handler, container, name): + o = urlparse("http://fake.host" + handler.path) + path = unquote_plus(o[2]) + handler.send_response(200) + handler.end_headers() + f = file(container['path'] + path[len(name)+1:], 'rb') + shutil.copyfileobj(f, handler.wfile) + + def get_local_path(self, handler, query): + + subcname = query['Container'][0] + container = handler.server.containers[subcname.split('/')[0]] + + path = container['path'] + for folder in subcname.split('/')[1:]: + if folder == '..': + return False + path = os.path.join(path, folder) + return path + + def get_files(self, handler, query, filterFunction=None): + subcname = query['Container'][0] + path = self.get_local_path(handler, query) + + files = os.listdir(path) + if filterFunction: + files = filter(filterFunction, files) + totalFiles = len(files) + + def dir_sort(x, y): + xdir = os.path.isdir(os.path.join(path, x)) + ydir = os.path.isdir(os.path.join(path, y)) + + if xdir and ydir: + return name_sort(x, y) + elif xdir: + return -1 + elif ydir: + return 1 + else: + return name_sort(x, y) + + def name_sort(x, y): + numbername = re.compile(r'(\d*)(.*)') + m = numbername.match(x) + xNumber = m.group(1) + xStr = m.group(2) + m = numbername.match(y) + yNumber = m.group(1) + yStr = m.group(2) + + if xNumber and yNumber: + xNumber, yNumber = int(xNumber), int(yNumber) + if xNumber == yNumber: + return cmp(xStr, yStr) + else: + return cmp(xNumber, yNumber) + elif xNumber: + return -1 + elif yNumber: + return 1 + else: + return cmp(xStr, yStr) + + files.sort(dir_sort) + + index = 0 + count = 10 + if query.has_key('ItemCount'): + count = int(query['ItemCount'] [0]) + + if query.has_key('AnchorItem'): + anchor = unquote(query['AnchorItem'][0]) + for i in range(len(files)): + if os.path.isdir(os.path.join(path,files[i])): + file_url = '/TiVoConnect?Command=QueryContainer&Container=' + subcname + '/' + files[i] + else: + file_url = '/' + subcname + '/' + files[i] + if file_url == anchor: + if count > 0: + index = i + 1 + elif count < 0: + index = i - 1 + else: + index = i + break + if query.has_key('AnchorOffset'): + index = index + int(query['AnchorOffset'][0]) + + #foward count + if index < index + count: + files = files[index:index + count ] + return files, totalFiles, index + #backwards count + else: + print 'index, count', index, count + print index + count + #off the start of the list + if index + count < 0: + print 0 - (index + count) + index += 0 - (index + count) + print index + count + files = files[index + count:index] + return files, totalFiles, index + count diff --git a/plugins/music/music.py b/plugins/music/music.py index 0399226..ccc9fd1 100644 --- a/plugins/music/music.py +++ b/plugins/music/music.py @@ -1,88 +1,88 @@ -import os, socket, re, sys -from Cheetah.Template import Template -from plugin import Plugin -from urllib import unquote_plus, quote, unquote -from xml.sax.saxutils import escape -from lrucache import LRUCache -import eyeD3 - -SCRIPTDIR = os.path.dirname(__file__) - -class music(Plugin): - - content_type = 'x-container/tivo-music' - playable_cache = {} - playable_cache = LRUCache(1000) - media_data_cache = LRUCache(100) - - def QueryContainer(self, handler, query): - - subcname = query['Container'][0] - cname = subcname.split('/')[0] - - if not handler.server.containers.has_key(cname) or not self.get_local_path(handler, query): - handler.send_response(404) - handler.end_headers() - return - - path = self.get_local_path(handler, query) - def isdir(file): - return os.path.isdir(os.path.join(path, file)) - - def AudioFileFilter(file): - full_path = os.path.join(path, file) - - if full_path in self.playable_cache: - return self.playable_cache[full_path] - if os.path.isdir(full_path) or eyeD3.isMp3File(full_path): - self.playable_cache[full_path] = True - return True - else: - self.playable_cache[full_path] = False - return False - - def media_data(file): - dict = {} - dict['path'] = file - - file = os.path.join(path, file) - - if file in self.media_data_cache: - return self.media_data_cache[file] - - if isdir(file) or not eyeD3.isMp3File(file): - self.media_data_cache[file] = dict - return dict - - try: - audioFile = eyeD3.Mp3AudioFile(file) - dict['Duration'] = audioFile.getPlayTime() * 1000 - dict['SourceBitRate'] = audioFile.getBitRate()[1] - dict['SourceSampleRate'] = audioFile.getSampleFreq() - - tag = audioFile.getTag() - dict['ArtistName'] = str(tag.getArtist()) - dict['AlbumTitle'] = str(tag.getAlbum()) - dict['SongTitle'] = str(tag.getTitle()) - dict['AlbumYear'] = tag.getYear() - - dict['MusicGenre'] = tag.getGenre().getName() - except: - pass - - self.media_data_cache[file] = dict - return dict - - handler.send_response(200) - handler.end_headers() - t = Template(file=os.path.join(SCRIPTDIR,'templates', 'container.tmpl')) - t.name = subcname - t.files, t.total, t.start = self.get_files(handler, query, AudioFileFilter) - t.files = map(media_data, t.files) - t.isdir = isdir - t.quote = quote - t.escape = escape - handler.wfile.write(t) - - - +import os, socket, re, sys +from Cheetah.Template import Template +from plugin import Plugin +from urllib import unquote_plus, quote, unquote +from xml.sax.saxutils import escape +from lrucache import LRUCache +import eyeD3 + +SCRIPTDIR = os.path.dirname(__file__) + +class music(Plugin): + + content_type = 'x-container/tivo-music' + playable_cache = {} + playable_cache = LRUCache(1000) + media_data_cache = LRUCache(100) + + def QueryContainer(self, handler, query): + + subcname = query['Container'][0] + cname = subcname.split('/')[0] + + if not handler.server.containers.has_key(cname) or not self.get_local_path(handler, query): + handler.send_response(404) + handler.end_headers() + return + + path = self.get_local_path(handler, query) + def isdir(file): + return os.path.isdir(os.path.join(path, file)) + + def AudioFileFilter(file): + full_path = os.path.join(path, file) + + if full_path in self.playable_cache: + return self.playable_cache[full_path] + if os.path.isdir(full_path) or eyeD3.isMp3File(full_path): + self.playable_cache[full_path] = True + return True + else: + self.playable_cache[full_path] = False + return False + + def media_data(file): + dict = {} + dict['path'] = file + + file = os.path.join(path, file) + + if file in self.media_data_cache: + return self.media_data_cache[file] + + if isdir(file) or not eyeD3.isMp3File(file): + self.media_data_cache[file] = dict + return dict + + try: + audioFile = eyeD3.Mp3AudioFile(file) + dict['Duration'] = audioFile.getPlayTime() * 1000 + dict['SourceBitRate'] = audioFile.getBitRate()[1] + dict['SourceSampleRate'] = audioFile.getSampleFreq() + + tag = audioFile.getTag() + dict['ArtistName'] = str(tag.getArtist()) + dict['AlbumTitle'] = str(tag.getAlbum()) + dict['SongTitle'] = str(tag.getTitle()) + dict['AlbumYear'] = tag.getYear() + + dict['MusicGenre'] = tag.getGenre().getName() + except: + pass + + self.media_data_cache[file] = dict + return dict + + handler.send_response(200) + handler.end_headers() + t = Template(file=os.path.join(SCRIPTDIR,'templates', 'container.tmpl')) + t.name = subcname + t.files, t.total, t.start = self.get_files(handler, query, AudioFileFilter) + t.files = map(media_data, t.files) + t.isdir = isdir + t.quote = quote + t.escape = escape + handler.wfile.write(t) + + + diff --git a/plugins/video/transcode.py b/plugins/video/transcode.py index 397f6ea..e7da180 100644 --- a/plugins/video/transcode.py +++ b/plugins/video/transcode.py @@ -1,318 +1,318 @@ -import subprocess, shutil, os, re, sys, ConfigParser, time, lrucache -import Config - -info_cache = lrucache.LRUCache(1000) - - -debug = Config.getDebug() -TIVO_WIDTH = Config.getTivoWidth() -TIVO_HEIGHT = Config.getTivoHeight() -AUDIO_BR = Config.getAudioBR() -VIDEO_BR = Config.getVideoBR() -FFMPEG = Config.get('Server', 'ffmpeg') - -def debug_write(data): - if debug: - debug_out = [] - debug_out.append('Transcode.py - ') - for x in data: - debug_out.append(str(x)) - fdebug = open('debug.txt', 'a') - fdebug.write(' '.join(debug_out)) - fdebug.close() - -# XXX BIG HACK -# subprocess is broken for me on windows so super hack -def patchSubprocess(): - o = subprocess.Popen._make_inheritable - - def _make_inheritable(self, handle): - if not handle: return subprocess.GetCurrentProcess() - return o(self, handle) - - subprocess.Popen._make_inheritable = _make_inheritable -mswindows = (sys.platform == "win32") -if mswindows: - patchSubprocess() - -def output_video(inFile, outFile, tsn=''): - if tivo_compatable(inFile): - debug_write(['output_video: ', inFile, ' is tivo compatible\n']) - f = file(inFile, 'rb') - shutil.copyfileobj(f, outFile) - f.close() - else: - debug_write(['output_video: ', inFile, ' is not tivo compatible\n']) - transcode(inFile, outFile, tsn) - -def transcode(inFile, outFile, tsn=''): - cmd = [FFMPEG, '-i', inFile, '-vcodec', 'mpeg2video', '-r', '29.97', '-b', VIDEO_BR] + select_aspect(inFile, tsn) + ['-comment', 'pyTivo.py', '-ac', '2', '-ab', AUDIO_BR,'-ar', '44100', '-f', 'vob', '-' ] - debug_write(['transcode: ffmpeg command is ', ''.join(cmd), '\n']) - ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE) - try: - shutil.copyfileobj(ffmpeg.stdout, outFile) - except: - kill(ffmpeg.pid) - -def select_aspect(inFile, tsn = ''): - type, width, height, fps, millisecs = video_info(inFile) - - debug_write(['tsn:', tsn, '\n']) - - aspect169 = Config.get169Setting(tsn) - - debug_write(['aspect169:', aspect169, '\n']) - - d = gcd(height,width) - ratio = (width*100)/height - rheight, rwidth = height/d, width/d - - debug_write(['select_aspect: File=', inFile, ' Type=', type, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, ' ratio=', ratio, ' rheight=', rheight, ' rwidth=', rwidth, '\n']) - - multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) - multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) - - if (rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54), (59, 72), (59, 36), (59, 54)]: - debug_write(['select_aspect: File is within 4:3 list.\n']) - return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)] - elif ((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), (59, 27)]) and aspect169: - debug_write(['select_aspect: File is within 16:9 list and 16:9 allowed.\n']) - return ['-aspect', '16:9', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)] - else: - settings = [] - #If video is wider than 4:3 add top and bottom padding - if (ratio > 133): #Might be 16:9 file, or just need padding on top and bottom - if aspect169 and (ratio > 135): #If file would fall in 4:3 assume it is supposed to be 4:3 - if (ratio > 177):#too short needs padding top and bottom - endHeight = int(((TIVO_WIDTH*height)/width) * multiplier16by9) - settings.append('-aspect') - settings.append('16:9') - if endHeight % 2: - endHeight -= 1 - if endHeight < TIVO_HEIGHT * 0.99: - settings.append('-s') - settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight)) - - topPadding = ((TIVO_HEIGHT - endHeight)/2) - if topPadding % 2: - topPadding -= 1 - - settings.append('-padtop') - settings.append(str(topPadding)) - bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding - settings.append('-padbottom') - settings.append(str(bottomPadding)) - else: #if only very small amount of padding needed, then just stretch it - settings.append('-s') - settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)) - debug_write(['select_aspect: 16:9 aspect allowed, file is wider than 16:9 padding top and bottom\n', ' '.join(settings), '\n']) - else: #too skinny needs padding on left and right. - endWidth = int((TIVO_HEIGHT*width)/(height*multiplier16by9)) - settings.append('-aspect') - settings.append('16:9') - if endWidth % 2: - endWidth -= 1 - if endWidth < (TIVO_WIDTH-10): - settings.append('-s') - settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT)) - - leftPadding = ((TIVO_WIDTH - endWidth)/2) - if leftPadding % 2: - leftPadding -= 1 - - settings.append('-padleft') - settings.append(str(leftPadding)) - rightPadding = (TIVO_WIDTH - endWidth) - leftPadding - settings.append('-padright') - settings.append(str(rightPadding)) - else: #if only very small amount of padding needed, then just stretch it - settings.append('-s') - settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)) - debug_write(['select_aspect: 16:9 aspect allowed, file is narrower than 16:9 padding left and right\n', ' '.join(settings), '\n']) - else: #this is a 4:3 file or 16:9 output not allowed - settings.append('-aspect') - settings.append('4:3') - endHeight = int(((TIVO_WIDTH*height)/width) * multiplier4by3) - if endHeight % 2: - endHeight -= 1 - if endHeight < TIVO_HEIGHT * 0.99: - settings.append('-s') - settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight)) - - topPadding = ((TIVO_HEIGHT - endHeight)/2) - if topPadding % 2: - topPadding -= 1 - - settings.append('-padtop') - settings.append(str(topPadding)) - bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding - settings.append('-padbottom') - settings.append(str(bottomPadding)) - else: #if only very small amount of padding needed, then just stretch it - settings.append('-s') - settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)) - debug_write(['select_aspect: File is wider than 4:3 padding top and bottom\n', ' '.join(settings), '\n']) - - return settings - #If video is taller than 4:3 add left and right padding, this is rare. All of these files will always be sent in - #an aspect ratio of 4:3 since they are so narrow. - else: - endWidth = int((TIVO_HEIGHT*width)/(height*multiplier4by3)) - settings.append('-aspect') - settings.append('4:3') - if endWidth % 2: - endWidth -= 1 - if endWidth < (TIVO_WIDTH * 0.99): - settings.append('-s') - settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT)) - - leftPadding = ((TIVO_WIDTH - endWidth)/2) - if leftPadding % 2: - leftPadding -= 1 - - settings.append('-padleft') - settings.append(str(leftPadding)) - rightPadding = (TIVO_WIDTH - endWidth) - leftPadding - settings.append('-padright') - settings.append(str(rightPadding)) - else: #if only very small amount of padding needed, then just stretch it - settings.append('-s') - settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)) - - debug_write(['select_aspect: File is taller than 4:3 padding left and right\n', ' '.join(settings), '\n']) - - return settings - -def tivo_compatable(inFile): - suportedModes = [[720, 480], [704, 480], [544, 480], [480, 480], [352, 480]] - type, width, height, fps, millisecs = video_info(inFile) - #print type, width, height, fps, millisecs - - if (inFile[-5:]).lower() == '.tivo': - debug_write(['tivo_compatible: ', inFile, ' ends with .tivo\n']) - return True - - if not type == 'mpeg2video': - #print 'Not Tivo Codec' - debug_write(['tivo_compatible: ', inFile, ' is not mpeg2video it is ', type, '\n']) - return False - - if not fps == '29.97': - #print 'Not Tivo fps' - debug_write(['tivo_compatible: ', inFile, ' is not correct fps it is ', fps, '\n']) - return False - - for mode in suportedModes: - if (mode[0], mode[1]) == (width, height): - #print 'Is TiVo!' - debug_write(['tivo_compatible: ', inFile, ' has correct width of ', width, ' and height of ', height, '\n']) - return True - #print 'Not Tivo dimensions' - return False - -def video_info(inFile): - mtime = os.stat(inFile).st_mtime - if inFile in info_cache and info_cache[inFile][0] == mtime: - debug_write(['video_info: ', inFile, ' cache hit!', '\n']) - return info_cache[inFile][1] - - if (inFile[-5:]).lower() == '.tivo': - info_cache[inFile] = (mtime, (True, True, True, True, True)) - debug_write(['video_info: ', inFile, ' ends in .tivo.\n']) - return True, True, True, True, True - - cmd = [FFMPEG, '-i', inFile ] - ffmpeg = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) - - # wait 4 sec if ffmpeg is not back give up - for i in range(80): - time.sleep(.05) - if not ffmpeg.poll() == None: - break - - if ffmpeg.poll() == None: - kill(ffmpeg.pid) - info_cache[inFile] = (mtime, (None, None, None, None, None)) - return None, None, None, None, None - - output = ffmpeg.stderr.read() - debug_write(['video_info: ffmpeg output=', output, '\n']) - - durre = re.compile(r'.*Duration: (.{2}):(.{2}):(.{2})\.(.),') - d = durre.search(output) - - rezre = re.compile(r'.*Video: ([^,]+),.*') - x = rezre.search(output) - if x: - codec = x.group(1) - else: - info_cache[inFile] = (mtime, (None, None, None, None, None)) - debug_write(['video_info: failed at codec\n']) - return None, None, None, None, None - - rezre = re.compile(r'.*Video: .+, (\d+)x(\d+),.*') - x = rezre.search(output) - if x: - width = int(x.group(1)) - height = int(x.group(2)) - else: - info_cache[inFile] = (mtime, (None, None, None, None, None)) - debug_write(['video_info: failed at width/height\n']) - return None, None, None, None, None - - rezre = re.compile(r'.*Video: .+, (.+) fps.*') - x = rezre.search(output) - if x: - fps = x.group(1) - else: - info_cache[inFile] = (mtime, (None, None, None, None, None)) - debug_write(['video_info: failed at fps\n']) - return None, None, None, None, None - - # Allow override only if it is mpeg2 and frame rate was doubled to 59.94 - if (not fps == '29.97') and (codec == 'mpeg2video'): - # First look for the build 7215 version - rezre = re.compile(r'.*film source: 29.97.*') - x = rezre.search(output.lower() ) - if x: - debug_write(['video_info: film source: 29.97 setting fps to 29.97\n']) - fps = '29.97' - else: - # for build 8047: - rezre = re.compile(r'.*frame rate differs from container frame rate: 29.97.*') - debug_write(['video_info: Bug in VideoReDo\n']) - x = rezre.search(output.lower() ) - if x: - fps = '29.97' - - millisecs = ((int(d.group(1))*3600) + (int(d.group(2))*60) + int(d.group(3)))*1000 + (int(d.group(4))*100) - info_cache[inFile] = (mtime, (codec, width, height, fps, millisecs)) - debug_write(['video_info: Codec=', codec, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, '\n']) - return codec, width, height, fps, millisecs - -def suported_format(inFile): - if video_info(inFile)[0]: - return True - else: - debug_write(['supported_format: ', inFile, ' is not supported\n']) - return False - -def kill(pid): - debug_write(['kill: killing pid=', str(pid), '\n']) - if mswindows: - win32kill(pid) - else: - import os, signal - os.kill(pid, signal.SIGKILL) - -def win32kill(pid): - import ctypes - handle = ctypes.windll.kernel32.OpenProcess(1, False, pid) - ctypes.windll.kernel32.TerminateProcess(handle, -1) - ctypes.windll.kernel32.CloseHandle(handle) - -def gcd(a,b): - while b: - a, b = b, a % b - return a - +import subprocess, shutil, os, re, sys, ConfigParser, time, lrucache +import Config + +info_cache = lrucache.LRUCache(1000) + + +debug = Config.getDebug() +TIVO_WIDTH = Config.getTivoWidth() +TIVO_HEIGHT = Config.getTivoHeight() +AUDIO_BR = Config.getAudioBR() +VIDEO_BR = Config.getVideoBR() +FFMPEG = Config.get('Server', 'ffmpeg') + +def debug_write(data): + if debug: + debug_out = [] + debug_out.append('Transcode.py - ') + for x in data: + debug_out.append(str(x)) + fdebug = open('debug.txt', 'a') + fdebug.write(' '.join(debug_out)) + fdebug.close() + +# XXX BIG HACK +# subprocess is broken for me on windows so super hack +def patchSubprocess(): + o = subprocess.Popen._make_inheritable + + def _make_inheritable(self, handle): + if not handle: return subprocess.GetCurrentProcess() + return o(self, handle) + + subprocess.Popen._make_inheritable = _make_inheritable +mswindows = (sys.platform == "win32") +if mswindows: + patchSubprocess() + +def output_video(inFile, outFile, tsn=''): + if tivo_compatable(inFile): + debug_write(['output_video: ', inFile, ' is tivo compatible\n']) + f = file(inFile, 'rb') + shutil.copyfileobj(f, outFile) + f.close() + else: + debug_write(['output_video: ', inFile, ' is not tivo compatible\n']) + transcode(inFile, outFile, tsn) + +def transcode(inFile, outFile, tsn=''): + cmd = [FFMPEG, '-i', inFile, '-vcodec', 'mpeg2video', '-r', '29.97', '-b', VIDEO_BR] + select_aspect(inFile, tsn) + ['-comment', 'pyTivo.py', '-ac', '2', '-ab', AUDIO_BR,'-ar', '44100', '-f', 'vob', '-' ] + debug_write(['transcode: ffmpeg command is ', ''.join(cmd), '\n']) + ffmpeg = subprocess.Popen(cmd, stdout=subprocess.PIPE) + try: + shutil.copyfileobj(ffmpeg.stdout, outFile) + except: + kill(ffmpeg.pid) + +def select_aspect(inFile, tsn = ''): + type, width, height, fps, millisecs = video_info(inFile) + + debug_write(['tsn:', tsn, '\n']) + + aspect169 = Config.get169Setting(tsn) + + debug_write(['aspect169:', aspect169, '\n']) + + d = gcd(height,width) + ratio = (width*100)/height + rheight, rwidth = height/d, width/d + + debug_write(['select_aspect: File=', inFile, ' Type=', type, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, ' ratio=', ratio, ' rheight=', rheight, ' rwidth=', rwidth, '\n']) + + multiplier16by9 = (16.0 * TIVO_HEIGHT) / (9.0 * TIVO_WIDTH) + multiplier4by3 = (4.0 * TIVO_HEIGHT) / (3.0 * TIVO_WIDTH) + + if (rwidth, rheight) in [(4, 3), (10, 11), (15, 11), (59, 54), (59, 72), (59, 36), (59, 54)]: + debug_write(['select_aspect: File is within 4:3 list.\n']) + return ['-aspect', '4:3', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)] + elif ((rwidth, rheight) in [(16, 9), (20, 11), (40, 33), (118, 81), (59, 27)]) and aspect169: + debug_write(['select_aspect: File is within 16:9 list and 16:9 allowed.\n']) + return ['-aspect', '16:9', '-s', str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)] + else: + settings = [] + #If video is wider than 4:3 add top and bottom padding + if (ratio > 133): #Might be 16:9 file, or just need padding on top and bottom + if aspect169 and (ratio > 135): #If file would fall in 4:3 assume it is supposed to be 4:3 + if (ratio > 177):#too short needs padding top and bottom + endHeight = int(((TIVO_WIDTH*height)/width) * multiplier16by9) + settings.append('-aspect') + settings.append('16:9') + if endHeight % 2: + endHeight -= 1 + if endHeight < TIVO_HEIGHT * 0.99: + settings.append('-s') + settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight)) + + topPadding = ((TIVO_HEIGHT - endHeight)/2) + if topPadding % 2: + topPadding -= 1 + + settings.append('-padtop') + settings.append(str(topPadding)) + bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding + settings.append('-padbottom') + settings.append(str(bottomPadding)) + else: #if only very small amount of padding needed, then just stretch it + settings.append('-s') + settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)) + debug_write(['select_aspect: 16:9 aspect allowed, file is wider than 16:9 padding top and bottom\n', ' '.join(settings), '\n']) + else: #too skinny needs padding on left and right. + endWidth = int((TIVO_HEIGHT*width)/(height*multiplier16by9)) + settings.append('-aspect') + settings.append('16:9') + if endWidth % 2: + endWidth -= 1 + if endWidth < (TIVO_WIDTH-10): + settings.append('-s') + settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT)) + + leftPadding = ((TIVO_WIDTH - endWidth)/2) + if leftPadding % 2: + leftPadding -= 1 + + settings.append('-padleft') + settings.append(str(leftPadding)) + rightPadding = (TIVO_WIDTH - endWidth) - leftPadding + settings.append('-padright') + settings.append(str(rightPadding)) + else: #if only very small amount of padding needed, then just stretch it + settings.append('-s') + settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)) + debug_write(['select_aspect: 16:9 aspect allowed, file is narrower than 16:9 padding left and right\n', ' '.join(settings), '\n']) + else: #this is a 4:3 file or 16:9 output not allowed + settings.append('-aspect') + settings.append('4:3') + endHeight = int(((TIVO_WIDTH*height)/width) * multiplier4by3) + if endHeight % 2: + endHeight -= 1 + if endHeight < TIVO_HEIGHT * 0.99: + settings.append('-s') + settings.append(str(TIVO_WIDTH) + 'x' + str(endHeight)) + + topPadding = ((TIVO_HEIGHT - endHeight)/2) + if topPadding % 2: + topPadding -= 1 + + settings.append('-padtop') + settings.append(str(topPadding)) + bottomPadding = (TIVO_HEIGHT - endHeight) - topPadding + settings.append('-padbottom') + settings.append(str(bottomPadding)) + else: #if only very small amount of padding needed, then just stretch it + settings.append('-s') + settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)) + debug_write(['select_aspect: File is wider than 4:3 padding top and bottom\n', ' '.join(settings), '\n']) + + return settings + #If video is taller than 4:3 add left and right padding, this is rare. All of these files will always be sent in + #an aspect ratio of 4:3 since they are so narrow. + else: + endWidth = int((TIVO_HEIGHT*width)/(height*multiplier4by3)) + settings.append('-aspect') + settings.append('4:3') + if endWidth % 2: + endWidth -= 1 + if endWidth < (TIVO_WIDTH * 0.99): + settings.append('-s') + settings.append(str(endWidth) + 'x' + str(TIVO_HEIGHT)) + + leftPadding = ((TIVO_WIDTH - endWidth)/2) + if leftPadding % 2: + leftPadding -= 1 + + settings.append('-padleft') + settings.append(str(leftPadding)) + rightPadding = (TIVO_WIDTH - endWidth) - leftPadding + settings.append('-padright') + settings.append(str(rightPadding)) + else: #if only very small amount of padding needed, then just stretch it + settings.append('-s') + settings.append(str(TIVO_WIDTH) + 'x' + str(TIVO_HEIGHT)) + + debug_write(['select_aspect: File is taller than 4:3 padding left and right\n', ' '.join(settings), '\n']) + + return settings + +def tivo_compatable(inFile): + suportedModes = [[720, 480], [704, 480], [544, 480], [480, 480], [352, 480]] + type, width, height, fps, millisecs = video_info(inFile) + #print type, width, height, fps, millisecs + + if (inFile[-5:]).lower() == '.tivo': + debug_write(['tivo_compatible: ', inFile, ' ends with .tivo\n']) + return True + + if not type == 'mpeg2video': + #print 'Not Tivo Codec' + debug_write(['tivo_compatible: ', inFile, ' is not mpeg2video it is ', type, '\n']) + return False + + if not fps == '29.97': + #print 'Not Tivo fps' + debug_write(['tivo_compatible: ', inFile, ' is not correct fps it is ', fps, '\n']) + return False + + for mode in suportedModes: + if (mode[0], mode[1]) == (width, height): + #print 'Is TiVo!' + debug_write(['tivo_compatible: ', inFile, ' has correct width of ', width, ' and height of ', height, '\n']) + return True + #print 'Not Tivo dimensions' + return False + +def video_info(inFile): + mtime = os.stat(inFile).st_mtime + if inFile in info_cache and info_cache[inFile][0] == mtime: + debug_write(['video_info: ', inFile, ' cache hit!', '\n']) + return info_cache[inFile][1] + + if (inFile[-5:]).lower() == '.tivo': + info_cache[inFile] = (mtime, (True, True, True, True, True)) + debug_write(['video_info: ', inFile, ' ends in .tivo.\n']) + return True, True, True, True, True + + cmd = [FFMPEG, '-i', inFile ] + ffmpeg = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) + + # wait 4 sec if ffmpeg is not back give up + for i in range(80): + time.sleep(.05) + if not ffmpeg.poll() == None: + break + + if ffmpeg.poll() == None: + kill(ffmpeg.pid) + info_cache[inFile] = (mtime, (None, None, None, None, None)) + return None, None, None, None, None + + output = ffmpeg.stderr.read() + debug_write(['video_info: ffmpeg output=', output, '\n']) + + durre = re.compile(r'.*Duration: (.{2}):(.{2}):(.{2})\.(.),') + d = durre.search(output) + + rezre = re.compile(r'.*Video: ([^,]+),.*') + x = rezre.search(output) + if x: + codec = x.group(1) + else: + info_cache[inFile] = (mtime, (None, None, None, None, None)) + debug_write(['video_info: failed at codec\n']) + return None, None, None, None, None + + rezre = re.compile(r'.*Video: .+, (\d+)x(\d+),.*') + x = rezre.search(output) + if x: + width = int(x.group(1)) + height = int(x.group(2)) + else: + info_cache[inFile] = (mtime, (None, None, None, None, None)) + debug_write(['video_info: failed at width/height\n']) + return None, None, None, None, None + + rezre = re.compile(r'.*Video: .+, (.+) fps.*') + x = rezre.search(output) + if x: + fps = x.group(1) + else: + info_cache[inFile] = (mtime, (None, None, None, None, None)) + debug_write(['video_info: failed at fps\n']) + return None, None, None, None, None + + # Allow override only if it is mpeg2 and frame rate was doubled to 59.94 + if (not fps == '29.97') and (codec == 'mpeg2video'): + # First look for the build 7215 version + rezre = re.compile(r'.*film source: 29.97.*') + x = rezre.search(output.lower() ) + if x: + debug_write(['video_info: film source: 29.97 setting fps to 29.97\n']) + fps = '29.97' + else: + # for build 8047: + rezre = re.compile(r'.*frame rate differs from container frame rate: 29.97.*') + debug_write(['video_info: Bug in VideoReDo\n']) + x = rezre.search(output.lower() ) + if x: + fps = '29.97' + + millisecs = ((int(d.group(1))*3600) + (int(d.group(2))*60) + int(d.group(3)))*1000 + (int(d.group(4))*100) + info_cache[inFile] = (mtime, (codec, width, height, fps, millisecs)) + debug_write(['video_info: Codec=', codec, ' width=', width, ' height=', height, ' fps=', fps, ' millisecs=', millisecs, '\n']) + return codec, width, height, fps, millisecs + +def suported_format(inFile): + if video_info(inFile)[0]: + return True + else: + debug_write(['supported_format: ', inFile, ' is not supported\n']) + return False + +def kill(pid): + debug_write(['kill: killing pid=', str(pid), '\n']) + if mswindows: + win32kill(pid) + else: + import os, signal + os.kill(pid, signal.SIGKILL) + +def win32kill(pid): + import ctypes + handle = ctypes.windll.kernel32.OpenProcess(1, False, pid) + ctypes.windll.kernel32.TerminateProcess(handle, -1) + ctypes.windll.kernel32.CloseHandle(handle) + +def gcd(a,b): + while b: + a, b = b, a % b + return a + diff --git a/plugins/video/video.py b/plugins/video/video.py index 02a4f9a..157b7fa 100644 --- a/plugins/video/video.py +++ b/plugins/video/video.py @@ -1,295 +1,295 @@ -import transcode, os, socket, re -from Cheetah.Template import Template -from plugin import Plugin -from urllib import unquote_plus, quote, unquote -from urlparse import urlparse -from xml.sax.saxutils import escape -from lrucache import LRUCache -import Config -import time - -SCRIPTDIR = os.path.dirname(__file__) -debug = Config.getDebug() -hack83 = Config.getHack83() -def debug_write(data): - if debug: - debug_out = [] - debug_out.append('Video.py - ') - for x in data: - debug_out.append(str(x)) - fdebug = open('debug.txt', 'a') - fdebug.write(' '.join(debug_out)) - fdebug.close() -if hack83: - debug_write(['Hack83 is enabled.\n']) - -class video(Plugin): - count = 0 - - content_type = 'x-container/tivo-videos' - - # Used for 8.3's broken requests - request_history = {} - - def SendFile(self, handler, container, name): - - #No longer a 'cheep' hack :p - if handler.headers.getheader('Range') and not handler.headers.getheader('Range') == 'bytes=0-': - handler.send_response(206) - handler.send_header('Connection', 'close') - handler.send_header('Content-Type', 'video/x-tivo-mpeg') - handler.send_header('Transfer-Encoding', 'chunked') - handler.send_header('Server', 'TiVo Server/1.4.257.475') - handler.end_headers() - handler.wfile.write("\x30\x0D\x0A") - return - - tsn = handler.headers.getheader('tsn', '') - - o = urlparse("http://fake.host" + handler.path) - path = unquote_plus(o[2]) - handler.send_response(200) - handler.end_headers() - transcode.output_video(container['path'] + path[len(name)+1:], handler.wfile, tsn) - - def hack(self, handler, query, subcname): - debug_write(['Hack new request ------------------------', '\n']) - debug_write(['Hack TiVo request is: \n', query, '\n']) - queryAnchor = '' - rightAnchor = '' - leftAnchor = '' - tsn = handler.headers.getheader('tsn', '') - - #not a tivo - if not tsn: - debug_write(['Hack this was not a TiVo request.', '\n']) - return query, None - - #this breaks up the anchor item request into seperate parts - if 'AnchorItem' in query and (query['AnchorItem']) != ['Hack8.3']: - if "".join(query['AnchorItem']).find('Container=') >= 0: - #This is a folder - queryAnchor = unquote_plus("".join(query['AnchorItem'])).split('Container=')[-1] - (leftAnchor, rightAnchor) = queryAnchor.rsplit('/', 1) - else: - #This is a file - queryAnchor = unquote_plus("".join(query['AnchorItem'])).split('/',1)[-1] - (leftAnchor, rightAnchor) = queryAnchor.rsplit('/', 1) - debug_write(['Hack queryAnchor: ', queryAnchor, ' leftAnchor: ', leftAnchor, ' rightAnchor: ', rightAnchor, '\n']) - - try: - path, state, = self.request_history[tsn] - except KeyError: - #Never seen this tsn, starting new history - debug_write(['New TSN.', '\n']) - path = [] - state = {} - self.request_history[tsn] = (path, state) - state['query'] = query - state['page'] = '' - state['time'] = int(time.time()) + 1000 - - debug_write(['Hack our saved request is: \n', state['query'], '\n']) - - current_folder = subcname.split('/')[-1] - - #Needed to get list of files - def VideoFileFilter(file): - full_path = os.path.join(filePath, file) - - if os.path.isdir(full_path): - return True - return transcode.suported_format(full_path) - - #Begin figuring out what the request TiVo sent us means - #There are 7 options that can occur - - #1. at the root - This request is always accurate - if len(subcname.split('/')) == 1: - debug_write(['Hack we are at the root. Saving query, Clearing state[page].', '\n']) - path[:] = [current_folder] - state['query'] = query - state['page'] = '' - return state['query'], path - - #2. entering a new folder - #If there is no AnchorItem in the request then we must - #be entering a new folder. - if 'AnchorItem' not in query: - debug_write(['Hack we are entering a new folder. Saving query, setting time, setting state[page].', '\n']) - path[:] = subcname.split('/') - state['query'] = query - state['time'] = int(time.time()) - filePath = self.get_local_path(handler, state['query']) - files, total, start = self.get_files(handler, state['query'], VideoFileFilter) - if len(files) >= 1: - state['page'] = files[0] - else: - state['page'] = '' - return state['query'], path - - #3. Request a page after pyTivo sent a 302 code - #we know this is the proper page - if "".join(query['AnchorItem']) == 'Hack8.3': - debug_write(['Hack requested page from 302 code. Returning saved query, ', '\n']) - return state['query'], path - - #4. this is a request for a file - if 'ItemCount' in query and int("".join(query['ItemCount'])) == 1: - debug_write(['Hack requested a file', '\n']) - #Everything in this request is right except the container - query['Container'] = ["/".join(path)] - return query, path - - ##All remaining requests could be a second erroneous request - #for each of the following we will pause to see if a correct - #request is coming right behind it. - - #Sleep just in case the erroneous request came first - #this allows a proper request to be processed first - debug_write(['Hack maybe erroneous request, sleeping.', '\n']) - time.sleep(.25) - - #5. scrolling in a folder - #This could be a request to exit a folder - #or scroll up or down within the folder - #First we have to figure out if we are scrolling - if 'AnchorOffset' in query: - debug_write(['Hack Anchor offset was in query. leftAnchor needs to match ', "/".join(path), '\n']) - if leftAnchor == str("/".join(path)): - debug_write(['Hack leftAnchor matched.', '\n']) - query['Container'] = ["/".join(path)] - filePath = self.get_local_path(handler, query) - files, total, start = self.get_files(handler, query, VideoFileFilter) - debug_write(['Hack saved page is= ', state['page'], ' top returned file is= ', files[0], '\n']) - #If the first file returned equals the top of the page - #then we haven't scrolled pages - if files[0] != str(state['page']): - debug_write(['Hack this is scrolling within a folder.', '\n']) - filePath = self.get_local_path(handler, query) - files, total, start = self.get_files(handler, query, VideoFileFilter) - state['page'] = files[0] - return query, path - - #The only remaining options are exiting a folder or - #this is a erroneous second request. - - #6. this an extraneous request - #this came within a second of a valid request - #just use that request. - if (int(time.time()) - state['time']) <= 1: - debug_write(['Hack erroneous request, send a 302 error', '\n']) - filePath = self.get_local_path(handler, query) - files, total, start = self.get_files(handler, query, VideoFileFilter) - return None, path - #7. this is a request to exit a folder - #this request came by itself it must be to exit a folder - else: - debug_write(['Hack over 1 second, must be request to exit folder', '\n']) - path.pop() - downQuery = {} - downQuery['Command'] = query['Command'] - downQuery['SortOrder'] = query['SortOrder'] - downQuery['ItemCount'] = query['ItemCount'] - downQuery['Filter'] = query['Filter'] - downQuery['Container'] = ["/".join(path)] - state['query'] = downQuery - return None, path - - #just in case we missed something. - debug_write(['Hack ERROR, should not have made it here. Trying to recover.', '\n']) - return state['query'], path - - def QueryContainer(self, handler, query): - - subcname = query['Container'][0] - - ##If you are running 8.3 software you want to enable hack83 in the config file - if hack83: - print '=========================================================================' - query, hackPath = self.hack(handler, query, subcname) - print 'Tivo said: ' + subcname + ' || Hack said: ' + "/".join(hackPath) - debug_write(['Hack Tivo said: ', subcname, ' || Hack said: ' , "/".join(hackPath), '\n']) - subcname = "/".join(hackPath) - - if not query: - debug_write(['Hack sending 302 redirect page', '\n']) - handler.send_response(302) - handler.send_header('Location ', 'http://' + handler.headers.getheader('host') + '/TiVoConnect?Command=QueryContainer&AnchorItem=Hack8.3&Container=' + "/".join(hackPath)) - handler.end_headers() - return - #End Hack mess - - keys = query.keys() - keys.sort() - - cname = subcname.split('/')[0] - - if not handler.server.containers.has_key(cname) or not self.get_local_path(handler, query): - handler.send_response(404) - handler.end_headers() - return - - path = self.get_local_path(handler, query) - def isdir(file): - return os.path.isdir(os.path.join(path, file)) - - def duration(file): - full_path = os.path.join(path, file) - return transcode.video_info(full_path)[4] - - def est_size(file): - full_path = os.path.join(path, file) - #Size is estimated by taking audio and video bit rate adding 2% - - if transcode.tivo_compatable(full_path): # Is TiVo compatible mpeg2 - return int(os.stat(full_path).st_size) - else: # Must be re-encoded - audioBPS = strtod(Config.getAudioBR()) - videoBPS = strtod(Config.getVideoBR()) - bitrate = audioBPS + videoBPS - return int((duration(file)/1000)*(bitrate * 1.02 / 8)) - - def VideoFileFilter(file): - full_path = os.path.join(path, file) - - if os.path.isdir(full_path): - return True - return transcode.suported_format(full_path) - - handler.send_response(200) - handler.end_headers() - t = Template(file=os.path.join(SCRIPTDIR,'templates', 'container.tmpl')) - t.name = subcname - t.files, t.total, t.start = self.get_files(handler, query, VideoFileFilter) - t.duration = duration - t.est_size = est_size - t.isdir = isdir - t.quote = quote - t.escape = escape - handler.wfile.write(t) - - -# Parse a bitrate using the SI/IEEE suffix values as if by ffmpeg -# For example, 2K==2000, 2Ki==2048, 2MB==16000000, 2MiB==16777216 -# Algorithm: http://svn.mplayerhq.hu/ffmpeg/trunk/libavcodec/eval.c -def strtod(value): - prefixes = {"y":-24,"z":-21,"a":-18,"f":-15,"p":-12,"n":-9,"u":-6,"m":-3,"c":-2,"d":-1,"h":2,"k":3,"K":3,"M":6,"G":9,"T":12,"P":15,"E":18,"Z":21,"Y":24} - p = re.compile(r'^(\d+)(?:([yzafpnumcdhkKMGTPEZY])(i)?)?([Bb])?$') - m = p.match(value) - if m is None: - raise SyntaxError('Invalid bit value syntax') - (coef, prefix, power, byte) = m.groups() - if prefix is None: - value = float(coef) - else: - exponent = float(prefixes[prefix]) - if power == "i": - # Use powers of 2 - value = float(coef) * pow(2.0, exponent/0.3) - else: - # Use powers of 10 - value = float(coef) * pow(10.0, exponent) - if byte == "B": # B==Byte, b=bit - value *= 8; - return value +import transcode, os, socket, re +from Cheetah.Template import Template +from plugin import Plugin +from urllib import unquote_plus, quote, unquote +from urlparse import urlparse +from xml.sax.saxutils import escape +from lrucache import LRUCache +import Config +import time + +SCRIPTDIR = os.path.dirname(__file__) +debug = Config.getDebug() +hack83 = Config.getHack83() +def debug_write(data): + if debug: + debug_out = [] + debug_out.append('Video.py - ') + for x in data: + debug_out.append(str(x)) + fdebug = open('debug.txt', 'a') + fdebug.write(' '.join(debug_out)) + fdebug.close() +if hack83: + debug_write(['Hack83 is enabled.\n']) + +class video(Plugin): + count = 0 + + content_type = 'x-container/tivo-videos' + + # Used for 8.3's broken requests + request_history = {} + + def SendFile(self, handler, container, name): + + #No longer a 'cheep' hack :p + if handler.headers.getheader('Range') and not handler.headers.getheader('Range') == 'bytes=0-': + handler.send_response(206) + handler.send_header('Connection', 'close') + handler.send_header('Content-Type', 'video/x-tivo-mpeg') + handler.send_header('Transfer-Encoding', 'chunked') + handler.send_header('Server', 'TiVo Server/1.4.257.475') + handler.end_headers() + handler.wfile.write("\x30\x0D\x0A") + return + + tsn = handler.headers.getheader('tsn', '') + + o = urlparse("http://fake.host" + handler.path) + path = unquote_plus(o[2]) + handler.send_response(200) + handler.end_headers() + transcode.output_video(container['path'] + path[len(name)+1:], handler.wfile, tsn) + + def hack(self, handler, query, subcname): + debug_write(['Hack new request ------------------------', '\n']) + debug_write(['Hack TiVo request is: \n', query, '\n']) + queryAnchor = '' + rightAnchor = '' + leftAnchor = '' + tsn = handler.headers.getheader('tsn', '') + + #not a tivo + if not tsn: + debug_write(['Hack this was not a TiVo request.', '\n']) + return query, None + + #this breaks up the anchor item request into seperate parts + if 'AnchorItem' in query and (query['AnchorItem']) != ['Hack8.3']: + if "".join(query['AnchorItem']).find('Container=') >= 0: + #This is a folder + queryAnchor = unquote_plus("".join(query['AnchorItem'])).split('Container=')[-1] + (leftAnchor, rightAnchor) = queryAnchor.rsplit('/', 1) + else: + #This is a file + queryAnchor = unquote_plus("".join(query['AnchorItem'])).split('/',1)[-1] + (leftAnchor, rightAnchor) = queryAnchor.rsplit('/', 1) + debug_write(['Hack queryAnchor: ', queryAnchor, ' leftAnchor: ', leftAnchor, ' rightAnchor: ', rightAnchor, '\n']) + + try: + path, state, = self.request_history[tsn] + except KeyError: + #Never seen this tsn, starting new history + debug_write(['New TSN.', '\n']) + path = [] + state = {} + self.request_history[tsn] = (path, state) + state['query'] = query + state['page'] = '' + state['time'] = int(time.time()) + 1000 + + debug_write(['Hack our saved request is: \n', state['query'], '\n']) + + current_folder = subcname.split('/')[-1] + + #Needed to get list of files + def VideoFileFilter(file): + full_path = os.path.join(filePath, file) + + if os.path.isdir(full_path): + return True + return transcode.suported_format(full_path) + + #Begin figuring out what the request TiVo sent us means + #There are 7 options that can occur + + #1. at the root - This request is always accurate + if len(subcname.split('/')) == 1: + debug_write(['Hack we are at the root. Saving query, Clearing state[page].', '\n']) + path[:] = [current_folder] + state['query'] = query + state['page'] = '' + return state['query'], path + + #2. entering a new folder + #If there is no AnchorItem in the request then we must + #be entering a new folder. + if 'AnchorItem' not in query: + debug_write(['Hack we are entering a new folder. Saving query, setting time, setting state[page].', '\n']) + path[:] = subcname.split('/') + state['query'] = query + state['time'] = int(time.time()) + filePath = self.get_local_path(handler, state['query']) + files, total, start = self.get_files(handler, state['query'], VideoFileFilter) + if len(files) >= 1: + state['page'] = files[0] + else: + state['page'] = '' + return state['query'], path + + #3. Request a page after pyTivo sent a 302 code + #we know this is the proper page + if "".join(query['AnchorItem']) == 'Hack8.3': + debug_write(['Hack requested page from 302 code. Returning saved query, ', '\n']) + return state['query'], path + + #4. this is a request for a file + if 'ItemCount' in query and int("".join(query['ItemCount'])) == 1: + debug_write(['Hack requested a file', '\n']) + #Everything in this request is right except the container + query['Container'] = ["/".join(path)] + return query, path + + ##All remaining requests could be a second erroneous request + #for each of the following we will pause to see if a correct + #request is coming right behind it. + + #Sleep just in case the erroneous request came first + #this allows a proper request to be processed first + debug_write(['Hack maybe erroneous request, sleeping.', '\n']) + time.sleep(.25) + + #5. scrolling in a folder + #This could be a request to exit a folder + #or scroll up or down within the folder + #First we have to figure out if we are scrolling + if 'AnchorOffset' in query: + debug_write(['Hack Anchor offset was in query. leftAnchor needs to match ', "/".join(path), '\n']) + if leftAnchor == str("/".join(path)): + debug_write(['Hack leftAnchor matched.', '\n']) + query['Container'] = ["/".join(path)] + filePath = self.get_local_path(handler, query) + files, total, start = self.get_files(handler, query, VideoFileFilter) + debug_write(['Hack saved page is= ', state['page'], ' top returned file is= ', files[0], '\n']) + #If the first file returned equals the top of the page + #then we haven't scrolled pages + if files[0] != str(state['page']): + debug_write(['Hack this is scrolling within a folder.', '\n']) + filePath = self.get_local_path(handler, query) + files, total, start = self.get_files(handler, query, VideoFileFilter) + state['page'] = files[0] + return query, path + + #The only remaining options are exiting a folder or + #this is a erroneous second request. + + #6. this an extraneous request + #this came within a second of a valid request + #just use that request. + if (int(time.time()) - state['time']) <= 1: + debug_write(['Hack erroneous request, send a 302 error', '\n']) + filePath = self.get_local_path(handler, query) + files, total, start = self.get_files(handler, query, VideoFileFilter) + return None, path + #7. this is a request to exit a folder + #this request came by itself it must be to exit a folder + else: + debug_write(['Hack over 1 second, must be request to exit folder', '\n']) + path.pop() + downQuery = {} + downQuery['Command'] = query['Command'] + downQuery['SortOrder'] = query['SortOrder'] + downQuery['ItemCount'] = query['ItemCount'] + downQuery['Filter'] = query['Filter'] + downQuery['Container'] = ["/".join(path)] + state['query'] = downQuery + return None, path + + #just in case we missed something. + debug_write(['Hack ERROR, should not have made it here. Trying to recover.', '\n']) + return state['query'], path + + def QueryContainer(self, handler, query): + + subcname = query['Container'][0] + + ##If you are running 8.3 software you want to enable hack83 in the config file + if hack83: + print '=========================================================================' + query, hackPath = self.hack(handler, query, subcname) + print 'Tivo said: ' + subcname + ' || Hack said: ' + "/".join(hackPath) + debug_write(['Hack Tivo said: ', subcname, ' || Hack said: ' , "/".join(hackPath), '\n']) + subcname = "/".join(hackPath) + + if not query: + debug_write(['Hack sending 302 redirect page', '\n']) + handler.send_response(302) + handler.send_header('Location ', 'http://' + handler.headers.getheader('host') + '/TiVoConnect?Command=QueryContainer&AnchorItem=Hack8.3&Container=' + "/".join(hackPath)) + handler.end_headers() + return + #End Hack mess + + keys = query.keys() + keys.sort() + + cname = subcname.split('/')[0] + + if not handler.server.containers.has_key(cname) or not self.get_local_path(handler, query): + handler.send_response(404) + handler.end_headers() + return + + path = self.get_local_path(handler, query) + def isdir(file): + return os.path.isdir(os.path.join(path, file)) + + def duration(file): + full_path = os.path.join(path, file) + return transcode.video_info(full_path)[4] + + def est_size(file): + full_path = os.path.join(path, file) + #Size is estimated by taking audio and video bit rate adding 2% + + if transcode.tivo_compatable(full_path): # Is TiVo compatible mpeg2 + return int(os.stat(full_path).st_size) + else: # Must be re-encoded + audioBPS = strtod(Config.getAudioBR()) + videoBPS = strtod(Config.getVideoBR()) + bitrate = audioBPS + videoBPS + return int((duration(file)/1000)*(bitrate * 1.02 / 8)) + + def VideoFileFilter(file): + full_path = os.path.join(path, file) + + if os.path.isdir(full_path): + return True + return transcode.suported_format(full_path) + + handler.send_response(200) + handler.end_headers() + t = Template(file=os.path.join(SCRIPTDIR,'templates', 'container.tmpl')) + t.name = subcname + t.files, t.total, t.start = self.get_files(handler, query, VideoFileFilter) + t.duration = duration + t.est_size = est_size + t.isdir = isdir + t.quote = quote + t.escape = escape + handler.wfile.write(t) + + +# Parse a bitrate using the SI/IEEE suffix values as if by ffmpeg +# For example, 2K==2000, 2Ki==2048, 2MB==16000000, 2MiB==16777216 +# Algorithm: http://svn.mplayerhq.hu/ffmpeg/trunk/libavcodec/eval.c +def strtod(value): + prefixes = {"y":-24,"z":-21,"a":-18,"f":-15,"p":-12,"n":-9,"u":-6,"m":-3,"c":-2,"d":-1,"h":2,"k":3,"K":3,"M":6,"G":9,"T":12,"P":15,"E":18,"Z":21,"Y":24} + p = re.compile(r'^(\d+)(?:([yzafpnumcdhkKMGTPEZY])(i)?)?([Bb])?$') + m = p.match(value) + if m is None: + raise SyntaxError('Invalid bit value syntax') + (coef, prefix, power, byte) = m.groups() + if prefix is None: + value = float(coef) + else: + exponent = float(prefixes[prefix]) + if power == "i": + # Use powers of 2 + value = float(coef) * pow(2.0, exponent/0.3) + else: + # Use powers of 10 + value = float(coef) * pow(10.0, exponent) + if byte == "B": # B==Byte, b=bit + value *= 8; + return value diff --git a/pyTivo.conf b/pyTivo.conf index c793161..14755d8 100644 --- a/pyTivo.conf +++ b/pyTivo.conf @@ -1,51 +1,51 @@ -[Server] -port=9032 - - -#Full path to ffmpeg including filename -#For windows: ffmpeg=c:\Program Files\pyTivo\plugins\video\ffmpeg_mp2.exe -#For linux: ffmpeg=/usr/bin/ffmpeg -ffmpeg=C:\Documents and Settings\Armooo\Desktop\pyTivoSrc\plugins\video\ffmpeg_mp2.exe - -#This will make a large debug.txt file in you base directory. It only debugs -#transcode.py right now. -debug=true - -# Audio bit-rate, default 192K -#audio_br=192K - -# Video bit-rate, default 4096K -#video_br=3Mi - -#Beacon broadcast address(es) -#Typically use 255.255.255.255 but on some multihomed machines you may -#need to specify the subnet broadcast address(es) of your Tivo boxes -#beacon=255.255.255.255 - -#Output Pixel Width: if you have an HDTV you might want to try 720 or 704 -#Valid: 720, 704, 544, 480, 352 -#width=704 -width=544 - -##Per tivo options -# section named _tivo_TSN with the tsn in all caps -#[_tivo_2400000DEADBEEF] - -#If you want to use 16:9 or 4:3 on this tivo -#aspect169=true - -[MyMovies] -#Type can be either 'video' or 'music' -type=video - -#Path is the full path to your files (No trailing slash needed) -#For windows: path=c:\videos -#For linux: path=/media -path=d:\video - - -##You can have more than one share -#[MyTelevision] -#type=video -#path=d:\television - +[Server] +port=9032 + + +#Full path to ffmpeg including filename +#For windows: ffmpeg=c:\Program Files\pyTivo\plugins\video\ffmpeg_mp2.exe +#For linux: ffmpeg=/usr/bin/ffmpeg +ffmpeg=C:\Documents and Settings\Armooo\Desktop\pyTivoSrc\plugins\video\ffmpeg_mp2.exe + +#This will make a large debug.txt file in you base directory. It only debugs +#transcode.py right now. +debug=true + +# Audio bit-rate, default 192K +#audio_br=192K + +# Video bit-rate, default 4096K +#video_br=3Mi + +#Beacon broadcast address(es) +#Typically use 255.255.255.255 but on some multihomed machines you may +#need to specify the subnet broadcast address(es) of your Tivo boxes +#beacon=255.255.255.255 + +#Output Pixel Width: if you have an HDTV you might want to try 720 or 704 +#Valid: 720, 704, 544, 480, 352 +#width=704 +width=544 + +##Per tivo options +# section named _tivo_TSN with the tsn in all caps +#[_tivo_2400000DEADBEEF] + +#If you want to use 16:9 or 4:3 on this tivo +#aspect169=true + +[MyMovies] +#Type can be either 'video' or 'music' +type=video + +#Path is the full path to your files (No trailing slash needed) +#For windows: path=c:\videos +#For linux: path=/media +path=d:\video + + +##You can have more than one share +#[MyTelevision] +#type=video +#path=d:\television + diff --git a/pyTivoConfigurator.py b/pyTivoConfigurator.py index 5fee059..d3feb03 100644 --- a/pyTivoConfigurator.py +++ b/pyTivoConfigurator.py @@ -1,120 +1,120 @@ -from Tkinter import * -import ConfigParser - -class pyTivoConfigurator(Frame): - - section = None - - def buildContainerList(self): - frame = Frame(self) - frame.pack(fill=Y, expand=1) - scrollbar = Scrollbar(frame, orient=VERTICAL) - self.container_list = Listbox(frame, yscrollcommand=scrollbar.set) - scrollbar.config(command=self.container_list.yview) - scrollbar.pack(side=RIGHT, fill=Y) - self.container_list.pack(side=LEFT, fill=BOTH, expand=1) - self.container_list.bind("", self.selected) - - def selected(self, e): - if not self.container_list.curselection(): - return - index = self.container_list.curselection()[0] - self.section = self.container_list.get(index) - - self.updatePath() - - def buildButtons(self): - frame = Frame() - frame.pack(fill=Y, expand=1) - - save_button = Button(frame, text="Save", command=self.save) - save_button.pack(side=RIGHT) - - add_button = Button(frame, text="Add", command=self.add) - add_button.pack(side=RIGHT) - - restart_button = Button(frame, text="Restart pyTivo", command=self.restart) - restart_button.pack(side=RIGHT) - - def save(self): - self.writeConfig() - - def add(self): - import tkSimpleDialog - sharename = tkSimpleDialog.askstring('Add share', 'Share Name') - self.config.add_section(sharename) - self.config.set(sharename, 'type', 'video') - self.config.set(sharename, 'path', '') - - self.updateContainerList() - - def restart(self): - import win32serviceutil - self.writeConfig() - win32serviceutil.RestartService('pyTivo') - - def buildPath(self): - frame = Frame(self) - frame.pack(fill=Y, expand=1) - l = Label(frame, text="Path") - l.pack(side=LEFT) - - button = Button(frame, text="Browse", command=self.setPath) - button.pack(side=RIGHT) - - self.path = Entry(frame) - self.path.pack(side=RIGHT, fill=Y, expand=1) - - - def setPath(self): - if not self.section: - return - import tkFileDialog - dir = tkFileDialog.askdirectory() - - self.config.set(self.section, 'path', dir) - self.updatePath() - - def updatePath(self): - if not self.section or not self.config.get(self.section, 'path'): - return - - self.path.delete(0, END) - self.path.insert(0, self.config.get(self.section, 'path')) - - def updateContainerList(self): - self.container_list.delete(0, END) - for section in self.config.sections(): - if not section == 'Server': - self.container_list.insert(END, section) - - def readConfig(self): - self.config = ConfigParser.ConfigParser() - self.config.read(self.config_file) - - def writeConfig(self): - self.config.write(open(self.config_file, 'w')) - - def __init__(self, master=None): - Frame.__init__(self, master) - self.master.title('pyTivoConfigurator') - self.pack() - - import os - p = os.path.dirname(__file__) - self.config_file = os.path.join(p, 'pyTivo.conf') - - self.readConfig() - - self.buildContainerList() - self.buildPath() - self.buildButtons() - - self.updateContainerList() - - - -if __name__ == '__main__': - root = Tk() - app = pyTivoConfigurator(master=root) - app.mainloop() +from Tkinter import * +import ConfigParser + +class pyTivoConfigurator(Frame): + + section = None + + def buildContainerList(self): + frame = Frame(self) + frame.pack(fill=Y, expand=1) + scrollbar = Scrollbar(frame, orient=VERTICAL) + self.container_list = Listbox(frame, yscrollcommand=scrollbar.set) + scrollbar.config(command=self.container_list.yview) + scrollbar.pack(side=RIGHT, fill=Y) + self.container_list.pack(side=LEFT, fill=BOTH, expand=1) + self.container_list.bind("", self.selected) + + def selected(self, e): + if not self.container_list.curselection(): + return + index = self.container_list.curselection()[0] + self.section = self.container_list.get(index) + + self.updatePath() + + def buildButtons(self): + frame = Frame() + frame.pack(fill=Y, expand=1) + + save_button = Button(frame, text="Save", command=self.save) + save_button.pack(side=RIGHT) + + add_button = Button(frame, text="Add", command=self.add) + add_button.pack(side=RIGHT) + + restart_button = Button(frame, text="Restart pyTivo", command=self.restart) + restart_button.pack(side=RIGHT) + + def save(self): + self.writeConfig() + + def add(self): + import tkSimpleDialog + sharename = tkSimpleDialog.askstring('Add share', 'Share Name') + self.config.add_section(sharename) + self.config.set(sharename, 'type', 'video') + self.config.set(sharename, 'path', '') + + self.updateContainerList() + + def restart(self): + import win32serviceutil + self.writeConfig() + win32serviceutil.RestartService('pyTivo') + + def buildPath(self): + frame = Frame(self) + frame.pack(fill=Y, expand=1) + l = Label(frame, text="Path") + l.pack(side=LEFT) + + button = Button(frame, text="Browse", command=self.setPath) + button.pack(side=RIGHT) + + self.path = Entry(frame) + self.path.pack(side=RIGHT, fill=Y, expand=1) + + + def setPath(self): + if not self.section: + return + import tkFileDialog + dir = tkFileDialog.askdirectory() + + self.config.set(self.section, 'path', dir) + self.updatePath() + + def updatePath(self): + if not self.section or not self.config.get(self.section, 'path'): + return + + self.path.delete(0, END) + self.path.insert(0, self.config.get(self.section, 'path')) + + def updateContainerList(self): + self.container_list.delete(0, END) + for section in self.config.sections(): + if not section == 'Server': + self.container_list.insert(END, section) + + def readConfig(self): + self.config = ConfigParser.ConfigParser() + self.config.read(self.config_file) + + def writeConfig(self): + self.config.write(open(self.config_file, 'w')) + + def __init__(self, master=None): + Frame.__init__(self, master) + self.master.title('pyTivoConfigurator') + self.pack() + + import os + p = os.path.dirname(__file__) + self.config_file = os.path.join(p, 'pyTivo.conf') + + self.readConfig() + + self.buildContainerList() + self.buildPath() + self.buildButtons() + + self.updateContainerList() + + + +if __name__ == '__main__': + root = Tk() + app = pyTivoConfigurator(master=root) + app.mainloop() -- 2.11.4.GIT