From 5bf1bd3d0d7775822c88bea395306a0c5d786f5b Mon Sep 17 00:00:00 2001 From: Jason Michalski Date: Sat, 3 Nov 2007 03:10:40 +0000 Subject: [PATCH] Fixed new lines --- Config.py | 228 +++++++-------- plugin.py | 258 ++++++++--------- plugins/music/music.py | 176 ++++++------ plugins/video/transcode.py | 670 ++++++++++++++++++++++----------------------- plugins/video/video.py | 224 +++++++-------- pyTivoConfigurator.py | 240 ++++++++-------- 6 files changed, 898 insertions(+), 898 deletions(-) diff --git a/Config.py b/Config.py index 3309597..2848ae6 100644 --- a/Config.py +++ b/Config.py @@ -1,114 +1,114 @@ -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 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(tsn = None): - if tsn and config.has_section('_tivo_' + tsn): - try: - return config.get('_tivo_' + tsn, 'audio_br') - except NoOptionError: - pass - - try: - return config.get('Server', 'audio_br') - except NoOptionError: #default to 192 - return '192K' - -def getVideoBR(tsn = None): - if tsn and config.has_section('_tivo_' + tsn): - try: - return config.get('_tivo_' + tsn, 'video_br') - except NoOptionError: - pass - - try: - return config.get('Server', 'video_br') - except NoOptionError: #default to 4096K - return '4096K' - -def getMaxVideoBR(): - try: - return config.get('Server', 'max_video_br') - except NoOptionError: #default to 17M - return '17M' - -def getBuffSize(): - try: - return config.get('Server', 'bufsize') - except NoOptionError: #default 1024k - return '1024k' - +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 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(tsn = None): + if tsn and config.has_section('_tivo_' + tsn): + try: + return config.get('_tivo_' + tsn, 'audio_br') + except NoOptionError: + pass + + try: + return config.get('Server', 'audio_br') + except NoOptionError: #default to 192 + return '192K' + +def getVideoBR(tsn = None): + if tsn and config.has_section('_tivo_' + tsn): + try: + return config.get('_tivo_' + tsn, 'video_br') + except NoOptionError: + pass + + try: + return config.get('Server', 'video_br') + except NoOptionError: #default to 4096K + return '4096K' + +def getMaxVideoBR(): + try: + return config.get('Server', 'max_video_br') + except NoOptionError: #default to 17M + return '17M' + +def getBuffSize(): + try: + return config.get('Server', 'bufsize') + except NoOptionError: #default 1024k + return '1024k' + 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 a58dfd9..7f3f6eb 100644 --- a/plugins/video/transcode.py +++ b/plugins/video/transcode.py @@ -1,335 +1,335 @@ -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() -MAX_VIDEO_BR = Config.getMaxVideoBR() -BUFF_SIZE = Config.getBuffSize() - -FFMPEG = Config.get('Server', 'ffmpeg') - -def debug_write(data): - if debug: - debug_out = [] - 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=''): - audio_br = Config.getAudioBR(tsn) - video_br = Config.getVideoBR(tsn) - - cmd = [ FFMPEG, - '-i', inFile, - '-vcodec', 'mpeg2video', - '-r', '29.97', - '-b', video_br, - '-maxrate', MAX_VIDEO_BR, - '-bufsize', BUFF_SIZE - ] + 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() +MAX_VIDEO_BR = Config.getMaxVideoBR() +BUFF_SIZE = Config.getBuffSize() + +FFMPEG = Config.get('Server', 'ffmpeg') + +def debug_write(data): + if debug: + debug_out = [] + 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=''): + audio_br = Config.getAudioBR(tsn) + video_br = Config.getVideoBR(tsn) + + cmd = [ FFMPEG, + '-i', inFile, + '-vcodec', 'mpeg2video', + '-r', '29.97', + '-b', video_br, + '-maxrate', MAX_VIDEO_BR, + '-bufsize', BUFF_SIZE + ] + 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 51a82b0..eab6ac3 100644 --- a/plugins/video/video.py +++ b/plugins/video/video.py @@ -1,112 +1,112 @@ -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 - -SCRIPTDIR = os.path.dirname(__file__) - - -class video(Plugin): - - content_type = 'x-container/tivo-videos' - - 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 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 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 + +SCRIPTDIR = os.path.dirname(__file__) + + +class video(Plugin): + + content_type = 'x-container/tivo-videos' + + 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 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 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/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