#!/usr/bin/env python
# Copyright 2006-2009 John-Mark Gurney <jmg@funkthat.com>

__version__ = '$Change$'
# $Id$

ffmpeg_path = '/a/home/jmg/src/ffmpeg/ffmpeg'
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

		mimetype = { 'xvid': 'video/x-msvideo',
			'mpeg2': 'video/mpeg',
			'mp4': 'video/mp4',
		}

		vcodec = mods[0]
		if mods[0] not in mimetype:
			vcodec = 'mp4'

		request.setHeader('content-type', mimetype[vcodec])
		if request.method == 'HEAD':
			return ''

		audiomp3 = [ '-acodec', 'mp3', '-ab', '192k', '-ac', '2', ]
		audiomp2 = [ '-acodec', 'mp2', '-ab', '256k', '-ac', '2', ]
		audioac3 = [ '-acodec', 'ac3', '-ab', '640k', ]
		audioaac = [ '-acodec', 'aac', '-ab', '640k', ]
		optdict = {
			'xvid':	[ '-vcodec', 'xvid',
				  #'-mv4', '-gmc', '-g', '240',
				  '-f', 'avi', ] + audiomp3,
			'mpeg2': [ '-vcodec', 'mpeg2video', #'-g', '60',
				   '-f', 'mpegts', ] + audioac3,
			'mp4': [ '-vcodec', 'libx264', #'-g', '60',
				   '-f', 'mpegts', ] + audioaac,
			}
		args = [ 'ffmpeg', '-i', path,
		    '-sameq',
		    '-threads', '4',
		    #'-vb', '8000k',
		    #'-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))