|  | #!/usr/bin/env python
# Copyright 2006 John-Mark Gurney <gurney_j@resnet.uoregon.edu>
__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, VideoItem, AudioItem, TextItem, ImageItem, Resource
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',
		'FSVideoItem', 'FSAudioItem', 'FSTextItem', 'FSImageItem',
		'mimetoklass',
	]
mimedict = static.loadMimeTypes()
_klassfuns = []
def registerklassfun(fun):
	_klassfuns.append(fun)
_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):
		# 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
			self.pstat = nstat
			self.doUpdate()
		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 None
			else:
				raise
		return self
	def doUpdate(self):
		raise NotImplementedError
	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 ''
		optdict = {
			'xvid':	[ '-vcodec', 'xvid',
				  #'-mv4', '-gmc', '-g', '240',
				  '-f', 'avi', ],
			'mpeg2': [ '-vcodec', 'mpeg2video', #'-g', '60',
				   '-f', 'mpeg', ],
			}
		audio = [ '-acodec', 'mp3', '-ab', '192', ]
		args = [ 'ffmpeg', '-i', path, '-b', '8000',
		    #'-sc_threshold', '500000', '-b_strategy', '1', '-max_b_frames', '6',
			] + optdict[vcodec] + audio + [ '-', ]
		#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['path'])
		del kwargs['path']
		mimetype = kwargs['mimetype']
		del kwargs['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
	def doUpdate(self):
		self.res = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
		self.res.size = os.path.getsize(self.FSpath)
		self.res = [ self.res ]
		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):
	if os.path.basename(path) in _filestoignore:
		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(cd, parent, path, name):
	klass = None
	fsname = os.path.join(path, name)
	try:
		fobj = open(fsname)
	except:
		fobj = None
	for i in itertools.chain(( ignoreFiles, ), _klassfuns, ( defFS, )):
		try:
			try:
				fobj.seek(0)	# incase the call expects a clean file
			except:
				pass
			#log.msg('testing:', `i`, `fsname`, `fobj`)
			klass, kwargs = i(fsname, fobj)
			if klass is not None:
				break
		except:
			#import traceback
			#traceback.print_exc(file=log.logfile)
			pass
	if klass is None or klass is IgnoreFile:
		return
	#log.msg('matched:', os.path.join(path, name), `i`, `klass`)
	return cd.addItem(parent, 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)
		# mapping from path to objectID
		self.pathObjmap = {}
	def doUpdate(self):
		# We need to rescan this dir, and see if our children has
		# changed any.
		doupdate = False
		children = sets.Set(os.listdir(self.FSpath))
		for i in self.pathObjmap.keys():
			if i not in children:
				doupdate = True
				# delete
				self.cd.delItem(self.pathObjmap[i])
				del self.pathObjmap[i]
		for i in children:
			if i in self.pathObjmap:
				continue
			# new object
			nf = dofileadd(self.cd, self.id, self.FSpath, i)
			if nf is not None:
				doupdate = True
				self.pathObjmap[i] = nf
		# sort our children
		self.sort(lambda x, y: cmp(x.title, y.title))
		# Pass up to handle UpdateID
		if doupdate:
			StorageFolder.doUpdate(self)
	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))
 |