Viewing file: media_server.py (29.51 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# -*- coding: utf-8 -*-
# Licensed under the MIT license # http://opensource.org/licenses/mit-license.php
# Copyright 2006,2007 Frank Scholz <coherence@beebits.net>
import os import re import traceback from StringIO import StringIO import urllib
from twisted.internet import task from twisted.internet import defer from twisted.web import static from twisted.web import resource, server #from twisted.web import proxy from twisted.python import util from twisted.python.filepath import FilePath
from coherence.extern.et import ET, indent
from coherence import __version__
from coherence.upnp.core.service import ServiceServer from coherence.upnp.core.utils import StaticFile from coherence.upnp.core.utils import ReverseProxyResource
from coherence.upnp.services.servers.connection_manager_server import ConnectionManagerServer from coherence.upnp.services.servers.content_directory_server import ContentDirectoryServer from coherence.upnp.services.servers.scheduled_recording_server import ScheduledRecordingServer from coherence.upnp.services.servers.media_receiver_registrar_server import MediaReceiverRegistrarServer from coherence.upnp.services.servers.media_receiver_registrar_server import FakeMediaReceiverRegistrarBackend
from coherence.upnp.devices.basics import BasicDeviceMixin
from coherence import log
COVER_REQUEST_INDICATOR = re.compile(".*?cover\.[A-Z|a-z]{3,4}$")
ATTACHMENT_REQUEST_INDICATOR = re.compile(".*?attachment=.*$")
TRANSCODED_REQUEST_INDICATOR = re.compile(".*/transcoded/.*$")
class MSRoot(resource.Resource, log.Loggable): logCategory = 'mediaserver'
def __init__(self, server, store): resource.Resource.__init__(self) log.Loggable.__init__(self) self.server = server self.store = store
#def delayed_response(self, resrc, request): # print "delayed_response", resrc, request # body = resrc.render(request) # print "delayed_response", body # if body == 1: # print "delayed_response not yet done" # return # request.setHeader("Content-length", str(len(body))) # request.write(response) # request.finish()
def getChildWithDefault(self, path, request): self.info('%s getChildWithDefault, %s, %s, %s %s' % (self.server.device_type, request.method, path, request.uri, request.client)) headers = request.getAllHeaders() self.msg( request.getAllHeaders())
try: if headers['getcontentfeatures.dlna.org'] != '1': request.setResponseCode(400) return static.Data('<html><p>wrong value for getcontentFeatures.dlna.org</p></html>','text/html') except: pass
if request.method == 'HEAD': if 'getcaptioninfo.sec' in headers: self.warning("requesting srt file for id %s" % path) ch = self.store.get_by_id(path) try: location = ch.get_path() caption = ch.caption if caption == None: raise KeyError request.setResponseCode(200) request.setHeader('CaptionInfo.sec', caption) return static.Data('','text/html') except: print traceback.format_exc() request.setResponseCode(404) return static.Data('<html><p>the requested srt file was not found</p></html>','text/html')
try: request._dlna_transfermode = headers['transfermode.dlna.org'] except KeyError: request._dlna_transfermode = 'Streaming' if request.method in ('GET','HEAD'): if COVER_REQUEST_INDICATOR.match(request.uri): self.info("request cover for id %s" % path) def got_item(ch): if ch is not None: request.setResponseCode(200) file = ch.get_cover() if os.path.exists(file): self.info("got cover %s" % file) return StaticFile(file) request.setResponseCode(404) return static.Data('<html><p>cover requested not found</p></html>','text/html')
dfr = defer.maybeDeferred(self.store.get_by_id, path) dfr.addCallback(got_item) return dfr
if ATTACHMENT_REQUEST_INDICATOR.match(request.uri): self.info("request attachment %r for id %s" % (request.args,path)) def got_attachment(ch): try: #FIXME same as below if 'transcoded' in request.args: if self.server.coherence.config.get('transcoding', 'no') == 'yes': format = request.args['transcoded'][0] type = request.args['type'][0] self.info("request transcoding %r %r" % (format, type)) try: from coherence.transcoder import TranscoderManager manager = TranscoderManager(self.server.coherence) return manager.select(format,ch.item.attachments[request.args['attachment'][0]]) except: self.debug(traceback.format_exc()) request.setResponseCode(404) return static.Data('<html><p>the requested transcoded file was not found</p></html>','text/html') else: request.setResponseCode(404) return static.Data("<html><p>This MediaServer doesn't support transcoding</p></html>",'text/html') else: return ch.item.attachments[request.args['attachment'][0]] except: request.setResponseCode(404) return static.Data('<html><p>the requested attachment was not found</p></html>','text/html') dfr = defer.maybeDeferred(self.store.get_by_id, path) dfr.addCallback(got_attachment) return dfr #if(request.method in ('GET','HEAD') and # XBOX_TRANSCODED_REQUEST_INDICATOR.match(request.uri)): # if self.server.coherence.config.get('transcoding', 'no') == 'yes': # id = path[:-15].split('/')[-1] # self.info("request transcoding to %r for id %s" % (request.args,id)) # ch = self.store.get_by_id(id) # uri = ch.get_path() # return MP3Transcoder(uri)
if(request.method in ('GET','HEAD') and TRANSCODED_REQUEST_INDICATOR.match(request.uri)): self.info("request transcoding to %s for id %s" % (request.uri.split('/')[-1],path)) if self.server.coherence.config.get('transcoding', 'no') == 'yes': def got_stuff_to_transcode(ch): #FIXME create a generic transcoder class and sort the details there format = request.uri.split('/')[-1] #request.args['transcoded'][0] uri = ch.get_path() try: from coherence.transcoder import TranscoderManager manager = TranscoderManager(self.server.coherence) return manager.select(format,uri) except: self.debug(traceback.format_exc()) request.setResponseCode(404) return static.Data('<html><p>the requested transcoded file was not found</p></html>','text/html') dfr = defer.maybeDeferred(self.store.get_by_id, path) dfr.addCallback(got_stuff_to_transcode) return dfr
request.setResponseCode(404) return static.Data("<html><p>This MediaServer doesn't support transcoding</p></html>",'text/html')
if(request.method == 'POST' and request.uri.endswith('?import')): d = self.import_file(path,request) if isinstance(d, defer.Deferred): d.addBoth(self.import_response,path) return d return self.import_response(None,path)
if(headers.has_key('user-agent') and (headers['user-agent'].find('Xbox/') == 0 or # XBox headers['user-agent'].startswith("""Mozilla/4.0 (compatible; UPnP/1.0; Windows""")) and # wmp11 path in ['description-1.xml','description-2.xml']): self.info('XBox/WMP alert, we need to simulate a Windows Media Connect server') if self.children.has_key('xbox-description-1.xml'): self.msg( 'returning xbox-description-1.xml') return self.children['xbox-description-1.xml']
if self.children.has_key(path): return self.children[path] if request.uri == '/': return self return self.getChild(path, request)
def requestFinished(self, result, id, request): self.info("finished, remove %d from connection table" % id) self.info("finished, sentLength: %d chunked: %d code: %d" % (request.sentLength, request.chunked, request.code)) self.info("finished %r" % request.headers) self.server.connection_manager_server.remove_connection(id)
def import_file(self,name,request): self.info("import file, id %s" % name) print "import file, id %s" % name def got_file(ch): print "ch", ch if ch is not None: if hasattr(self.store,'backend_import'): response_code = self.store.backend_import(ch,request.content) if isinstance(response_code, defer.Deferred): return response_code request.setResponseCode(response_code) return else: request.setResponseCode(404) dfr = defer.maybeDeferred(self.store.get_by_id, name) dfr.addCallback(got_file)
def prepare_connection(self,request): new_id,_,_ = self.server.connection_manager_server.add_connection('', 'Output', -1, '') self.info("startup, add %d to connection table" % new_id) d = request.notifyFinish() d.addBoth(self.requestFinished, new_id, request)
def prepare_headers(self,ch,request): request.setHeader('transferMode.dlna.org', request._dlna_transfermode) if hasattr(ch,'item') and hasattr(ch.item, 'res'): if ch.item.res[0].protocolInfo is not None: additional_info = ch.item.res[0].get_additional_info() if additional_info != '*': request.setHeader('contentFeatures.dlna.org', additional_info) elif 'getcontentfeatures.dlna.org' in request.getAllHeaders(): request.setHeader('contentFeatures.dlna.org', "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000")
def process_child(self,ch,name,request): if ch != None: self.info('Child found', ch) if(request.method == 'GET' or request.method == 'HEAD'): headers = request.getAllHeaders() if headers.has_key('content-length'): self.warning('%s request with content-length %s header - sanitizing' % ( request.method, headers['content-length'])) del request.received_headers['content-length'] self.debug('data', ) if len(request.content.getvalue()) > 0: """ shall we remove that? can we remove that? """ self.warning('%s request with %d bytes of message-body - sanitizing' % ( request.method, len(request.content.getvalue()))) request.content = StringIO()
if hasattr(ch, "location"): self.debug("we have a location %s" % isinstance(ch.location, resource.Resource)) if(isinstance(ch.location, ReverseProxyResource) or isinstance(ch.location, resource.Resource)): self.info('getChild proxy %s to %s' % (name, ch.location.uri)) self.prepare_connection(request) self.prepare_headers(ch,request) return ch.location try: p = ch.get_path() except TypeError: return self.list_content(name, ch, request) except Exception, msg: self.debug("error accessing items path %r" % msg) self.debug(traceback.format_exc()) return self.list_content(name, ch, request) if p != None and os.path.exists(p): self.info("accessing path %r" % p) self.prepare_connection(request) self.prepare_headers(ch,request) ch = StaticFile(p) else: self.debug("accessing path %r failed" % p) return self.list_content(name, ch, request)
if ch is None: p = util.sibpath(__file__, name) if os.path.exists(p): ch = StaticFile(p) self.info('MSRoot ch', ch) return ch
def getChild(self, name, request): self.info('getChild %s, %s' % (name, request)) ch = self.store.get_by_id(name) if isinstance(ch, defer.Deferred): ch.addCallback(self.process_child,name,request) #ch.addCallback(self.delayed_response, request) return ch return self.process_child(ch,name,request)
def list_content(self, name, item, request): self.info('list_content', name, item, request) page = """<html><head><title>%s</title></head><body><p>%s</p>"""% \ (item.get_name().encode('ascii','xmlcharrefreplace'), item.get_name().encode('ascii','xmlcharrefreplace'))
if( hasattr(item,'mimetype') and item.mimetype in ['directory','root']): uri = request.uri if uri[-1] != '/': uri += '/'
def build_page(r,page): #print "build_page", r page += """<ul>""" if r is not None: for c in r: if hasattr(c,'get_url'): path = c.get_url() self.debug('has get_url', path) elif hasattr(c,'get_path') and c.get_path != None: #path = c.get_path().encode('utf-8').encode('string_escape') path = c.get_path() if isinstance(path,unicode): path = path.encode('ascii','xmlcharrefreplace') else: path = path.decode('utf-8').encode('ascii','xmlcharrefreplace') self.debug('has get_path', path) else: path = request.uri.split('/') path[-1] = str(c.get_id()) path = '/'.join(path) self.debug('got path', path) title = c.get_name() self.debug( 'title is:', type(title)) try: if isinstance(title,unicode): title = title.encode('ascii','xmlcharrefreplace') else: title = title.decode('utf-8').encode('ascii','xmlcharrefreplace') except (UnicodeEncodeError,UnicodeDecodeError): title = c.get_name().encode('utf-8').encode('string_escape') page += '<li><a href="%s">%s</a></li>' % \ (path, title) page += """</ul>""" page += """</body></html>""" return static.Data(page,'text/html')
children = item.get_children() if isinstance(children, defer.Deferred): print "list_content, we have a Deferred", children children.addCallback(build_page,page) #children.addErrback(....) #FIXME return children
return build_page(children,page)
elif( hasattr(item,'mimetype') and item.mimetype.find('image/') == 0): #path = item.get_path().encode('utf-8').encode('string_escape') path = urllib.quote(item.get_path().encode('utf-8')) title = item.get_name().decode('utf-8').encode('ascii','xmlcharrefreplace') page += """<p><img src="%s" alt="%s"></p>""" % \ (path, title) else: pass page += """</body></html>""" return static.Data(page,'text/html')
def listchilds(self, uri): self.info('listchilds %s' % uri) if uri[-1] != '/': uri += '/' cl = '<p><a href=%s0>content</a></p>' % uri for c in self.children: cl += '<li><a href=%s%s>%s</a></li>' % (uri,c,c) return cl
def import_response(self,result,id): return static.Data('<html><p>import of %s finished</p></html>'% id,'text/html')
def render(self,request): #print "render", request return '<html><p>root of the %s MediaServer</p><p><ul>%s</ul></p></html>'% \ (self.server.backend, self.listchilds(request.uri))
class RootDeviceXML(static.Data):
def __init__(self, hostname, uuid, urlbase, device_type='MediaServer', version=2, friendly_name='Coherence UPnP A/V MediaServer', xbox_hack=False, services=[], devices=[], icons=[], presentationURL=None): uuid = str(uuid) root = ET.Element('root') root.attrib['xmlns']='urn:schemas-upnp-org:device-1-0' device_type = 'urn:schemas-upnp-org:device:%s:%d' % (device_type, int(version)) e = ET.SubElement(root, 'specVersion') ET.SubElement(e, 'major').text = '1' ET.SubElement(e, 'minor').text = '0'
#if version == 1: # ET.SubElement(root, 'URLBase').text = urlbase + uuid[5:] + '/'
d = ET.SubElement(root, 'device') ET.SubElement(d, 'deviceType').text = device_type if xbox_hack == False: ET.SubElement(d, 'friendlyName').text = friendly_name else: ET.SubElement(d, 'friendlyName').text = friendly_name + ' : 1 : Windows Media Connect' ET.SubElement(d, 'manufacturer').text = 'beebits.net' ET.SubElement(d, 'manufacturerURL').text = 'http://coherence.beebits.net' ET.SubElement(d, 'modelDescription').text = 'Coherence UPnP A/V MediaServer' if xbox_hack == False: ET.SubElement(d, 'modelName').text = 'Coherence UPnP A/V MediaServer' else: ET.SubElement(d, 'modelName').text = 'Windows Media Connect' ET.SubElement(d, 'modelNumber').text = __version__ ET.SubElement(d, 'modelURL').text = 'http://coherence.beebits.net' ET.SubElement(d, 'serialNumber').text = '0000001' ET.SubElement(d, 'UDN').text = uuid ET.SubElement(d, 'UPC').text = ''
if len(icons): e = ET.SubElement(d, 'iconList') for icon in icons:
icon_path = '' if icon.has_key('url'): if icon['url'].startswith('file://'): icon_path = icon['url'][7:] elif icon['url'] == '.face': icon_path = os.path.join(os.path.expanduser('~'), ".face") else: from pkg_resources import resource_filename icon_path = os.path.abspath(resource_filename(__name__, os.path.join('..','..','..','misc','device-icons',icon['url'])))
if os.path.exists(icon_path) == True: i = ET.SubElement(e, 'icon') for k,v in icon.items(): if k == 'url': if v.startswith('file://'): ET.SubElement(i, k).text = '/'+uuid[5:]+'/'+os.path.basename(v) continue elif v == '.face': ET.SubElement(i, k).text = '/'+uuid[5:]+'/'+'face-icon.png' continue else: ET.SubElement(i, k).text = '/'+uuid[5:]+'/'+os.path.basename(v) continue ET.SubElement(i, k).text = str(v)
if len(services): e = ET.SubElement(d, 'serviceList') for service in services: id = service.get_id() if xbox_hack == False and id == 'X_MS_MediaReceiverRegistrar': continue s = ET.SubElement(e, 'service') try: namespace = service.namespace except: namespace = 'schemas-upnp-org' if( hasattr(service,'version') and service.version < version): v = service.version else: v = version ET.SubElement(s, 'serviceType').text = 'urn:%s:service:%s:%d' % (namespace, id, int(v)) try: namespace = service.id_namespace except: namespace = 'upnp-org' ET.SubElement(s, 'serviceId').text = 'urn:%s:serviceId:%s' % (namespace,id) ET.SubElement(s, 'SCPDURL').text = '/' + uuid[5:] + '/' + id + '/' + service.scpd_url ET.SubElement(s, 'controlURL').text = '/' + uuid[5:] + '/' + id + '/' + service.control_url ET.SubElement(s, 'eventSubURL').text = '/' + uuid[5:] + '/' + id + '/' + service.subscription_url
#ET.SubElement(s, 'SCPDURL').text = id + '/' + service.scpd_url #ET.SubElement(s, 'controlURL').text = id + '/' + service.control_url #ET.SubElement(s, 'eventSubURL').text = id + '/' + service.subscription_url
if len(devices): e = ET.SubElement(d, 'deviceList')
if presentationURL is None: presentationURL = '/' + uuid[5:] ET.SubElement(d, 'presentationURL').text = presentationURL
x = ET.SubElement(d, 'dlna:X_DLNADOC') x.attrib['xmlns:dlna']='urn:schemas-dlna-org:device-1-0' x.text = 'DMS-1.50' x = ET.SubElement(d, 'dlna:X_DLNADOC') x.attrib['xmlns:dlna']='urn:schemas-dlna-org:device-1-0' x.text = 'M-DMS-1.50' x=ET.SubElement(d, 'dlna:X_DLNACAP') x.attrib['xmlns:dlna']='urn:schemas-dlna-org:device-1-0' x.text = 'av-upload,image-upload,audio-upload'
#if self.has_level(LOG_DEBUG): # indent( root) self.xml = """<?xml version="1.0" encoding="utf-8"?>""" + ET.tostring( root, encoding='utf-8') static.Data.__init__(self, self.xml, 'text/xml')
class MediaServer(log.Loggable,BasicDeviceMixin): logCategory = 'mediaserver'
device_type = 'MediaServer' presentationURL = None
def fire(self,backend,**kwargs):
if kwargs.get('no_thread_needed',False) == False: """ this could take some time, put it in a thread to be sure it doesn't block as we can't tell for sure that every backend is implemented properly """
from twisted.internet import threads d = threads.deferToThread(backend, self, **kwargs)
def backend_ready(backend): self.backend = backend
def backend_failure(x): self.warning('backend %s not installed, MediaServer activation aborted - %s', backend, x.getErrorMessage()) self.debug(x)
d.addCallback(backend_ready) d.addErrback(backend_failure)
# FIXME: we need a timeout here so if the signal we wait for not arrives we'll # can close down this device else: self.backend = backend(self, **kwargs)
def init_complete(self, backend): if self.backend != backend: return self._services = [] self._devices = []
try: self.connection_manager_server = ConnectionManagerServer(self) self._services.append(self.connection_manager_server) except LookupError,msg: self.warning( 'ConnectionManagerServer', msg) raise LookupError,msg
try: transcoding = False if self.coherence.config.get('transcoding', 'no') == 'yes': transcoding = True self.content_directory_server = ContentDirectoryServer(self,transcoding=transcoding) self._services.append(self.content_directory_server) except LookupError,msg: self.warning( 'ContentDirectoryServer', msg) raise LookupError,msg
try: self.media_receiver_registrar_server = MediaReceiverRegistrarServer(self, backend=FakeMediaReceiverRegistrarBackend()) self._services.append(self.media_receiver_registrar_server) except LookupError,msg: self.warning( 'MediaReceiverRegistrarServer (optional)', msg)
try: self.scheduled_recording_server = ScheduledRecordingServer(self) self._services.append(self.scheduled_recording_server) except LookupError,msg: self.info( 'ScheduledRecordingServer', msg)
upnp_init = getattr(self.backend, "upnp_init", None) if upnp_init: upnp_init()
self.web_resource = MSRoot( self, backend) self.coherence.add_web_resource( str(self.uuid)[5:], self.web_resource)
version = int(self.version) while version > 0: self.web_resource.putChild( 'description-%d.xml' % version, RootDeviceXML( self.coherence.hostname, str(self.uuid), self.coherence.urlbase, self.device_type, version, friendly_name=self.backend.name, services=self._services, devices=self._devices, icons=self.icons, presentationURL = self.presentationURL)) self.web_resource.putChild( 'xbox-description-%d.xml' % version, RootDeviceXML( self.coherence.hostname, str(self.uuid), self.coherence.urlbase, self.device_type, version, friendly_name=self.backend.name, xbox_hack=True, services=self._services, devices=self._devices, icons=self.icons, presentationURL = self.presentationURL)) version -= 1
self.web_resource.putChild('ConnectionManager', self.connection_manager_server) self.web_resource.putChild('ContentDirectory', self.content_directory_server) if hasattr(self,"scheduled_recording_server"): self.web_resource.putChild('ScheduledRecording', self.scheduled_recording_server) if hasattr(self,"media_receiver_registrar_server"): self.web_resource.putChild('X_MS_MediaReceiverRegistrar', self.media_receiver_registrar_server)
for icon in self.icons: if icon.has_key('url'): if icon['url'].startswith('file://'): if os.path.exists(icon['url'][7:]): self.web_resource.putChild(os.path.basename(icon['url']), StaticFile(icon['url'][7:],defaultType=icon['mimetype'])) elif icon['url'] == '.face': face_path = os.path.abspath(os.path.join(os.path.expanduser('~'), ".face")) if os.path.exists(face_path): self.web_resource.putChild('face-icon.png',StaticFile(face_path,defaultType=icon['mimetype'])) else: from pkg_resources import resource_filename icon_path = os.path.abspath(resource_filename(__name__, os.path.join('..','..','..','misc','device-icons',icon['url']))) if os.path.exists(icon_path): self.web_resource.putChild(icon['url'],StaticFile(icon_path,defaultType=icon['mimetype']))
self.register() self.warning("%s %s (%s) activated with %s" % (self.backend.name, self.device_type, self.backend, str(self.uuid)[5:]))
|