|  | #!/usr/bin/env python
# Copyright 2006-2009 John-Mark Gurney <jmg@funkthat.com>
__version__ = '$Change$'
# $Id$
ffmpeg_path = '/usr/local/bin/ffmpeg'
import FileDIDL
import errno
import itertools
import os
import sets
import stat
from DIDLLite import StorageFolder, Item, Resource, ResourceList
from twisted.web import resource, server, static
from twisted.python import log
from twisted.internet import abstract, interfaces, process, protocol, reactor
from zope.interface import implements
__all__ = [ 'registerklassfun', 'registerfiletoignore',
		'FSObject', 'FSItem', 'FSDirectory',
	]
mimedict = static.loadMimeTypes()
_klassfuns = []
def registerklassfun(fun, debug=False):
	_klassfuns.append((fun, debug))
_filestoignore = {
		'.DS_Store': None
	}
def registerfiletoignore(f):
	_filestoignore[f] = None
# Return this class when you want the file to be skipped.  If you return this,
# no other modules will be applied, and it won't be added.  Useful for things
# like .DS_Store which are known to useless on a media server.
class IgnoreFile:
	pass
def statcmp(a, b, cmpattrs = [ 'st_ino', 'st_dev', 'st_size', 'st_mtime', ]):
	if a is None or b is None:
		return False
	for i in cmpattrs:
		if getattr(a, i) != getattr(b, i):
			return False
	return True
class FSObject(object):
	def __init__(self, path):
		self.FSpath = path
		self.pstat = None
	def checkUpdate(self, **kwargs):
		# need to handle no such file or directory
		# push it up? but still need to handle disappearing
		try:
			nstat = os.stat(self.FSpath)
			if statcmp(self.pstat, nstat):
				return
			self.pstat = nstat
			self.doUpdate(**kwargs)
		except OSError, x:
			log.msg('os.stat, OSError: %s' % x)
			if x.errno in (errno.ENOENT, errno.ENOTDIR, errno.EPERM, ):
				# We can't access it anymore, delete it
				self.cd.delItem(self.id)
				return
			else:
				raise
	def __repr__(self):
		return '<%s.%s: path: %s, id: %s, parent: %s, title: %s>' % \
		    (self.__class__.__module__, self.__class__.__name__,
		    self.FSpath, self.id, self.parentID, self.title)
class NullConsumer(file, abstract.FileDescriptor):
	implements(interfaces.IConsumer)
	def __init__(self):
		file.__init__(self, '/dev/null', 'w')
		abstract.FileDescriptor.__init__(self)
	def write(self, data):
		pass
class DynamTransfer(protocol.ProcessProtocol):
	def __init__(self, path, mods, request):
		self.path = path
		self.mods = mods
		self.request = request
	def outReceived(self, data):
		self.request.write(data)
	def outConnectionLost(self):
		if self.request:
			self.request.unregisterProducer()
			self.request.finish()
			self.request = None
	def errReceived(self, data):
		pass
		#log.msg(data)
	def stopProducing(self):
		if self.request:
			self.request.unregisterProducer()
			self.request.finish()
		if self.proc:
			self.proc.loseConnection()
			self.proc.signalProcess('INT')
		self.request = None
		self.proc = None
	pauseProducing = lambda x: x.proc.pauseProducing()
	resumeProducing = lambda x: x.proc.resumeProducing()
	def render(self):
		mods = self.mods
		path = self.path
		request = self.request
		vcodec = mods[0]
		if mods[0] not in ('xvid', 'mpeg2', ):
			vcodec = 'xvid'
		mimetype = { 'xvid': 'video/avi', 'mpeg2': 'video/mpeg', }
		mimetype = { 'xvid': 'video/x-msvideo', 'mpeg2': 'video/mpeg', }
		request.setHeader('content-type', mimetype[vcodec])
		if request.method == 'HEAD':
			return ''
		audiomp3 = [ '-acodec', 'mp3', '-ab', '192', ]
		audiomp2 = [ '-acodec', 'mp2', '-ab', '256', ]
		optdict = {
			'xvid':	[ '-vcodec', 'xvid',
				  #'-mv4', '-gmc', '-g', '240',
				  '-f', 'avi', ] + audiomp3,
			'mpeg2': [ '-vcodec', 'mpeg2video', #'-g', '60',
				   '-f', 'mpeg', ] + audiomp2,
			}
		args = [ 'ffmpeg', '-i', path, '-b', '4000',
		    #'-sc_threshold', '500000', '-b_strategy', '1', '-max_b_frames', '6',
			] + optdict[vcodec] + [ '-', ]
		#log.msg(*[`i` for i in args])
		self.proc = process.Process(reactor, ffmpeg_path, args,
		    None, None, self)
		self.proc.closeStdin()
		request.registerProducer(self, 1)
		return server.NOT_DONE_YET
class DynamicTrans(resource.Resource):
	isLeaf = True
	def __init__(self, path, notrans):
		self.path = path
		self.notrans = notrans
	def render(self, request):
		#if request.getHeader('getcontentfeatures.dlna.org'):
		#	request.setHeader('contentFeatures.dlna.org', 'DLNA.ORG_OP=01;DLNA.ORG_CI=0')
		#	# we only want the headers
		#	self.notrans.render(request)
		#	request.unregisterProducer()  
		#	return ''
		if request.postpath:
			# Translation request
			return DynamTransfer(self.path, request.postpath, request).render()
		else:
			return self.notrans.render(request)
class FSItem(FSObject, Item):
	def __init__(self, *args, **kwargs):
		FSObject.__init__(self, kwargs.pop('path'))
		mimetype = kwargs.pop('mimetype')
		kwargs['content'] = DynamicTrans(self.FSpath,
		    static.File(self.FSpath, mimetype))
		Item.__init__(self, *args, **kwargs)
		self.url = '%s/%s' % (self.cd.urlbase, self.id)
		self.mimetype = mimetype
		self.checkUpdate()
	def doUpdate(self):
		#print 'FSItem doUpdate:', `self`
		self.res = ResourceList()
		r = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
		r.size = os.path.getsize(self.FSpath)
		self.res.append(r)
		if self.mimetype.split('/', 1)[0] == 'video':
			self.res.append(Resource(self.url + '/mpeg2',
			    'http-get:*:%s:*' % 'video/mpeg'))
			self.res.append(Resource(self.url + '/xvid',
			    'http-get:*:%s:*' % 'video/x-msvideo'))
		Item.doUpdate(self)
def ignoreFiles(path, fobj):
	bn = os.path.basename(path)
	if bn in _filestoignore:
		return IgnoreFile, None
	elif bn[:2] == '._' and open(path).read(4) == '\x00\x05\x16\x07':
		# AppleDouble encoded Macintosh Resource Fork
		return IgnoreFile, None
	return None, None
def defFS(path, fobj):
	if os.path.isdir(path):
		# new dir
		return FSDirectory, { 'path': path }
	elif os.path.isfile(path):
		# new file - fall through to below
		pass
	else:
		log.msg('skipping (not dir or reg): %s' % path)
		return None, None
	klass, mt = FileDIDL.buildClassMT(FSItem, path)
	return klass, { 'path': path, 'mimetype': mt }
def dofileadd(path, name):
	klass = None
	fsname = os.path.join(path, name)
	try:
		fobj = open(fsname)
	except:
		fobj = None
	for i, debug in itertools.chain(( (ignoreFiles, False), ), _klassfuns, ( (defFS, False), )):
		try:
			try:
				# incase the call expects a clean file
				fobj.seek(0)
			except:
				pass
			#log.msg('testing:', `i`, `fsname`, `fobj`)
			klass, kwargs = i(fsname, fobj)
			if klass is not None:
				break
		except:
			if debug:
				import traceback
				traceback.print_exc(file=log.logfile)
	if klass is None or klass is IgnoreFile:
		return None, None, None, None
	#print 'matched:', os.path.join(path, name), `i`, `klass`
	return klass, name, (), kwargs
class FSDirectory(FSObject, StorageFolder):
	def __init__(self, *args, **kwargs):
		path = kwargs['path']
		del kwargs['path']
		StorageFolder.__init__(self, *args, **kwargs)
		FSObject.__init__(self, path)
	def genCurrent(self):
		return ((x.id, os.path.basename(x.FSpath)) for x in self )
	def genChildren(self):
		return os.listdir(self.FSpath)
	def createObject(self, i):
		return dofileadd(self.FSpath, i)
	def __repr__(self):
		return ('<%s.%s: path: %s, id: %s, parent: %s, title: %s, ' + \
		    'cnt: %d>') % (self.__class__.__module__,
		    self.__class__.__name__, self.FSpath, self.id,
		    self.parentID, self.title, len(self))
 |