def __init__(self, server, **kwargs): BackendStore.__init__(self,server) self.next_id = 1000 self.name = kwargs.get('name','my media') self.content = kwargs.get('content',None) if self.content != None: if isinstance(self.content,basestring): self.content = [self.content] l = [] for a in self.content: l += a.split(',') self.content = l else: self.content = xdg_content() if self.content == None: self.content = 'tests/content' if not isinstance( self.content, list): self.content = [self.content] self.urlbase = kwargs.get('urlbase','') ignore_patterns = kwargs.get('ignore_patterns',[]) if self.urlbase[len(self.urlbase)-1] != '/': self.urlbase += '/' self.server = server self.store = {} try: self.inotify = INotify() except: self.inotify = None if kwargs.get('enable_destroy','no') == 'yes': self.upnp_DestroyObject = self.hidden_upnp_DestroyObject self.ignore_file_pattern = re.compile('|'.join(['^\..*'] + list(ignore_patterns))) parent = None self.update_id = 0 if len(self.content)>1: UPnPClass = classChooser('root') id = self.getnextID() parent = self.store[id] = FSItem( id, parent, 'media', 'root', self.urlbase, UPnPClass, update=True) for path in self.content: if isinstance(path,(list,tuple)): path = path[0] if self.ignore_file_pattern.match(path): continue self.walk(path, parent, self.ignore_file_pattern) self.wmc_mapping.update({'14': 0, '15': 0, '16': 0, '17': 0 }) louie.send('Coherence.UPnP.Backend.init_completed', None, backend=self)
def __init__(self, server, **kwargs): BackendStore.__init__(self, server, **kwargs) self.next_id = 1000 self.name = kwargs.get('name', 'my media') self.content = kwargs.get('content', None) if self.content is not None: if isinstance(self.content, str): self.content = [self.content] cl = [] for a in self.content: cl += a.split(',') self.content = cl else: self.content = xdg_content() self.content = [x[0] for x in self.content] if self.content is None: self.content = 'tests/content' if not isinstance(self.content, list): self.content = [self.content] self.content = set([os.path.abspath(x) for x in self.content]) ignore_patterns = kwargs.get('ignore_patterns', []) self.store = {} self.inotify = None if kwargs.get('enable_inotify', 'yes') == 'yes': if INotify: try: self.inotify = INotify() self.inotify.startReading() except Exception as msg: self.error(f'inotify disabled: {msg}') self.inotify = None else: self.info(f'{no_inotify_reason}') else: self.info('FSStore content auto-update disabled upon user request') if kwargs.get('enable_destroy', 'no') == 'yes': self.upnp_DestroyObject = self.hidden_upnp_DestroyObject self.import_folder = kwargs.get('import_folder', None) if self.import_folder is not None: self.import_folder = os.path.abspath(self.import_folder) if not os.path.isdir(self.import_folder): self.import_folder = None self.ignore_file_pattern = re.compile(r'|'.join([r'^\..*'] + list(ignore_patterns))) parent = None self.update_id = 0 if (len(self.content) > 1 or utils.means_true(kwargs.get('create_root', False)) or self.import_folder is not None): UPnPClass = classChooser('root') id = str(self.getnextID()) try: parent = self.store[id] = FSItem( id, parent, 'media', 'root', self.urlbase, UPnPClass, update=True, store=self, ) except Exception as e: self.error( f'Error on setting self.store[id], Error on FSItem: {e}') exit(1) if self.import_folder is not None: id = str(self.getnextID()) self.store[id] = FSItem( id, parent, self.import_folder, 'directory', self.urlbase, UPnPClass, update=True, store=self, ) self.import_folder_id = id for bytesPath in self.content: if isinstance(bytesPath, (list, tuple)): path = str(bytesPath[0]) else: path = str(bytesPath) if self.ignore_file_pattern.match(path): continue try: self.walk(path, parent, self.ignore_file_pattern) except Exception as msg: self.warning(f'on walk of {path!r}: {msg!r}') import traceback self.debug(traceback.format_exc()) self.wmc_mapping.update({'14': '0', '15': '0', '16': '0', '17': '0'}) self.init_completed = True
class FSStore(BackendStore): ''' .. versionchanged:: 0.9.0 Migrated from louie/dispatcher to EventDispatcher ''' logCategory = 'fs_store' implements = ['MediaServer'] description = '''MediaServer exporting files from the file-system''' options = [ { 'option': 'name', 'type': 'string', 'default': 'my media', 'help': 'the name under this MediaServer ' 'shall show up with on other UPnP clients', }, { 'option': 'version', 'type': 'int', 'default': 2, 'enum': (2, 1), 'help': 'the highest UPnP version this MediaServer shall support', 'level': 'advance', }, { 'option': 'uuid', 'type': 'string', 'help': 'the unique (UPnP) identifier for this MediaServer,' ' usually automatically set', 'level': 'advance', }, { 'option': 'content', 'type': 'string', 'default': xdg_content(), 'help': 'the path(s) this MediaServer shall export', }, { 'option': 'ignore_patterns', 'type': 'string', 'help': 'list of regex patterns, matching filenames will be ignored', # noqa: E501 }, { 'option': 'enable_inotify', 'type': 'string', 'default': 'yes', 'help': 'enable real-time monitoring of the content folders', }, { 'option': 'enable_destroy', 'type': 'string', 'default': 'no', 'help': 'enable deleting a file via an UPnP method', }, { 'option': 'import_folder', 'type': 'string', 'help': 'The path to store files imported via an UPnP method, ' 'if empty the Import method is disabled', }, ] def __init__(self, server, **kwargs): BackendStore.__init__(self, server, **kwargs) self.next_id = 1000 self.name = kwargs.get('name', 'my media') self.content = kwargs.get('content', None) if self.content is not None: if isinstance(self.content, str): self.content = [self.content] cl = [] for a in self.content: cl += a.split(',') self.content = cl else: self.content = xdg_content() self.content = [x[0] for x in self.content] if self.content is None: self.content = 'tests/content' if not isinstance(self.content, list): self.content = [self.content] self.content = set([os.path.abspath(x) for x in self.content]) ignore_patterns = kwargs.get('ignore_patterns', []) self.store = {} self.inotify = None if kwargs.get('enable_inotify', 'yes') == 'yes': if INotify: try: self.inotify = INotify() self.inotify.startReading() except Exception as msg: self.error(f'inotify disabled: {msg}') self.inotify = None else: self.info(f'{no_inotify_reason}') else: self.info('FSStore content auto-update disabled upon user request') if kwargs.get('enable_destroy', 'no') == 'yes': self.upnp_DestroyObject = self.hidden_upnp_DestroyObject self.import_folder = kwargs.get('import_folder', None) if self.import_folder is not None: self.import_folder = os.path.abspath(self.import_folder) if not os.path.isdir(self.import_folder): self.import_folder = None self.ignore_file_pattern = re.compile(r'|'.join([r'^\..*'] + list(ignore_patterns))) parent = None self.update_id = 0 if (len(self.content) > 1 or utils.means_true(kwargs.get('create_root', False)) or self.import_folder is not None): UPnPClass = classChooser('root') id = str(self.getnextID()) try: parent = self.store[id] = FSItem( id, parent, 'media', 'root', self.urlbase, UPnPClass, update=True, store=self, ) except Exception as e: self.error( f'Error on setting self.store[id], Error on FSItem: {e}') exit(1) if self.import_folder is not None: id = str(self.getnextID()) self.store[id] = FSItem( id, parent, self.import_folder, 'directory', self.urlbase, UPnPClass, update=True, store=self, ) self.import_folder_id = id for bytesPath in self.content: if isinstance(bytesPath, (list, tuple)): path = str(bytesPath[0]) else: path = str(bytesPath) if self.ignore_file_pattern.match(path): continue try: self.walk(path, parent, self.ignore_file_pattern) except Exception as msg: self.warning(f'on walk of {path!r}: {msg!r}') import traceback self.debug(traceback.format_exc()) self.wmc_mapping.update({'14': '0', '15': '0', '16': '0', '17': '0'}) self.init_completed = True def __repr__(self): return self.__class__.__name__ def release(self): if self.inotify is not None: self.inotify.stopReading() def len(self): return len(self.store) def get_by_id(self, id): # print('get_by_id', id, type(id)) # we have referenced ids here when we are in WMC mapping mode if isinstance(id, str): id = id.split('@', 1)[0] elif isinstance(id, bytes): id = id.decode('utf-8').split('@', 1)[0] elif isinstance(id, int): id = str(id) # try: # id = int(id) # except ValueError: # id = 1000 if id == '0': id = '1000' # print('get_by_id 2', id) try: r = self.store[id] except KeyError: r = None # print('get_by_id 3', r) return r def get_id_by_name(self, parent='0', name=''): self.info(f'get_id_by_name {parent} ({type(parent)}) {name}') try: parent = self.store[parent] self.debug(f'{parent} {len(parent.children):d}') for child in parent.children: # if not isinstance(name, unicode): # name = name.decode('utf8') self.debug(f'{child.get_name()} {child.get_realpath()} ' + f'{name == child.get_realpath()}') if name == child.get_realpath(): return child.id except Exception as e: self.error(f'get_id_by_name: {e!r}') import traceback self.info(traceback.format_exc()) self.debug('get_id_by_name not found') return None def get_url_by_name(self, parent='0', name=''): self.info(f'get_url_by_name {parent!r} {name!r}') id = self.get_id_by_name(parent, name) # print 'get_url_by_name', id if id is None: return '' return self.store[id].url def update_config(self, **kwargs): self.info(f'update_config: {kwargs}') if 'content' in kwargs: new_content = kwargs['content'] new_content = set( [os.path.abspath(x) for x in new_content.split(',')]) new_folders = new_content.difference(self.content) obsolete_folders = self.content.difference(new_content) self.debug(f'new folders: {new_folders}\n' f'obsolete folders: {obsolete_folders}') for folder in obsolete_folders: self.remove_content_folder(folder) for folder in new_folders: self.add_content_folder(folder) self.content = new_content def add_content_folder(self, path): path = os.path.abspath(path) if path not in self.content: self.content.add(path) self.walk(path, self.store['1000'], self.ignore_file_pattern) def remove_content_folder(self, path): path = os.path.abspath(path) if path in self.content: id = self.get_id_by_name('1000', path) self.remove(id) self.content.remove(path) def walk(self, path, parent=None, ignore_file_pattern=''): self.debug(f'walk {path}') containers = [] parent = self.append(path, parent) if parent is not None: containers.append(parent) while len(containers) > 0: container = containers.pop() try: self.debug(f'adding {container.location!r}') self.info(f'walk.adding: {container.location}') for child in container.location.children(): if ignore_file_pattern.match(child.basename()) is not None: continue new_container = self.append(child.path, container) if new_container is not None: containers.append(new_container) except UnicodeDecodeError: self.warning( f'UnicodeDecodeError - there is something wrong with a ' + f'file located in {container.get_path()!r}') def create(self, mimetype, path, parent): self.debug(f'create {mimetype} {path} {type(path)} {parent}') UPnPClass = classChooser(mimetype) if UPnPClass is None: return None id = self.getnextID() if mimetype in ('root', 'directory'): id = str(id) else: _, ext = os.path.splitext(path) id = str(id) + ext.lower() update = False if hasattr(self, 'update_id'): update = True self.store[id] = FSItem( id, parent, path, mimetype, self.urlbase, UPnPClass, update=True, store=self, ) if hasattr(self, 'update_id'): self.update_id += 1 # print(self.update_id) if self.server: if hasattr(self.server, 'content_directory_server'): self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) if parent is not None: value = (parent.get_id(), parent.get_update_id()) if self.server: if hasattr(self.server, 'content_directory_server'): self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value) return id def append(self, bytes_path, parent): path = str(bytes_path) self.debug(f'append {path} {type(path)} {parent}') if not os.path.exists(path): self.warning(f'path {path!r} not available - ignored') return None if stat.S_ISFIFO(os.stat(path).st_mode): self.warning(f'path {path!r} is a FIFO - ignored') return None try: mimetype, _ = mimetypes.guess_type(path, strict=False) if mimetype is None: if os.path.isdir(path): mimetype = 'directory' if mimetype is None: return None id = self.create(mimetype, path, parent) if mimetype == 'directory': if self.inotify is not None: mask = (IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_CHANGED) self.inotify.watch( FilePath(os.path.abspath(path)), mask=mask, autoAdd=False, callbacks=[partial(self.notify, parameter=id)], ) return self.store[id] except OSError as os_msg: # seems we have some permissions issues along the content path self.warning(f'path {path} isn\'t accessible, error {os_msg}') return None def remove(self, id): self.debug(f'FSSTore remove id: {id}') try: item = self.store[id] parent = item.get_parent() item.remove() del self.store[id] if hasattr(self, 'update_id'): self.update_id += 1 if self.server: self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) # value = f'{parent.get_id():d},{parent_get_update_id():d}' value = (parent.get_id(), parent.get_update_id()) if self.server: self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value) except KeyError: pass def notify(self, ignore, path, mask, parameter=None): self.info( 'Event %s on %s - parameter %r', ', '.join([fl for fl in _FLAG_TO_HUMAN if fl[0] == mask][0]), path.path, parameter, ) if mask & IN_CHANGED: # FIXME react maybe on access right changes, loss of read rights? # print(f'{path} was changed, parent {parameter:d} ({iwp.path})') pass if mask & IN_DELETE or mask & IN_MOVED_FROM: self.info(f'{path.path} was deleted, ' f'parent {parameter!r} ({path.parent.path})') id = self.get_id_by_name(parameter, path.path) if id is not None: self.remove(id) if mask & IN_CREATE or mask & IN_MOVED_TO: if mask & IN_ISDIR: self.info(f'directory {path.path} was created, ' f'parent {parameter!r} ({path.parent.path})') else: self.info(f'file {path.path} was created, ' f'parent {parameter!r} ({path.parent.path})') if self.get_id_by_name(parameter, path.path) is None: if path.isdir(): self.walk( path.path, self.get_by_id(parameter), self.ignore_file_pattern, ) else: if self.ignore_file_pattern.match(parameter) is None: self.append(str(path.path), str(self.get_by_id(parameter))) def getnextID(self): ret = self.next_id self.next_id += 1 return ret def backend_import(self, item, data): try: f = open(item.get_path(), 'w+b') if hasattr(data, 'read'): data = data.read() f.write(data) f.close() item.rebuild(self.urlbase) return 200 except IOError: self.warning(f'import of file {item.get_path()} failed') except Exception as msg: import traceback self.warning(traceback.format_exc()) return 500 def upnp_init(self): self.current_connection_id = None if self.server: self.server.connection_manager_server.set_variable( 0, 'SourceProtocolInfo', [ f'internal:{self.server.coherence.hostname}:audio/mpeg:*', 'http-get:*:audio/mpeg:*', f'internal:{self.server.coherence.hostname}:video/mp4:*', 'http-get:*:video/mp4:*', f'internal:{self.server.coherence.hostname}:application/ogg:*', # noqa: E501 'http-get:*:application/ogg:*', f'internal:{self.server.coherence.hostname}:video/x-msvideo:*', # noqa: E501 'http-get:*:video/x-msvideo:*', f'internal:{self.server.coherence.hostname}:video/mpeg:*', 'http-get:*:video/mpeg:*', f'internal:{self.server.coherence.hostname}:video/avi:*', 'http-get:*:video/avi:*', f'internal:{self.server.coherence.hostname}:video/divx:*', 'http-get:*:video/divx:*', f'internal:{self.server.coherence.hostname}:video/quicktime:*', # noqa: E501 'http-get:*:video/quicktime:*', f'internal:{self.server.coherence.hostname}:image/gif:*', 'http-get:*:image/gif:*', f'internal:{self.server.coherence.hostname}:image/jpeg:*', 'http-get:*:image/jpeg:*' # 'http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=11;' # 'DLNA.ORG_FLAGS=01700000000000000000000000000000', # 'http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;' # 'DLNA.ORG_OP=11;DLNA.ORG_FLAGS' # '=01700000000000000000000000000000', # 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;' # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS' # '=00f00000000000000000000000000000', # 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;' # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS' # '=00f00000000000000000000000000000', # 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;' # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS' # '=00f00000000000000000000000000000', # 'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;' # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS' # '=00f00000000000000000000000000000', # 'http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;' # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS' # '=01700000000000000000000000000000', # 'http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;' # 'DLNA.ORG_OP=01;DLNA.ORG_FLAGS' # '=01700000000000000000000000000000', ], default=True, ) self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) # self.server.content_directory_server.set_variable( # 0, 'SortCapabilities', '*') def upnp_ImportResource(self, *args, **kwargs): SourceURI = kwargs['SourceURI'] DestinationURI = kwargs['DestinationURI'] if DestinationURI.endswith('?import'): id = DestinationURI.split('/')[-1] id = id[:-7] # remove the ?import else: return failure.Failure(errorCode(718)) item = self.get_by_id(id) if item is None: return failure.Failure(errorCode(718)) def gotPage(headers): # print('gotPage', headers) content_type = headers.get('content-type', []) if not isinstance(content_type, list): content_type = list(content_type) if len(content_type) > 0: extension = mimetypes.guess_extension(content_type[0], strict=False) item.set_path(None, extension) shutil.move(tmp_path, item.get_path()) item.rebuild(self.urlbase) if hasattr(self, 'update_id'): self.update_id += 1 if self.server: if hasattr(self.server, 'content_directory_server'): self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) if item.parent is not None: value = (item.parent.get_id(), item.parent.get_update_id()) if self.server: if hasattr(self.server, 'content_directory_server'): self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value) def gotError(error, url): self.warning(f'error requesting {url}') self.info(error) os.unlink(tmp_path) return failure.Failure(errorCode(718)) tmp_fp, tmp_path = tempfile.mkstemp() os.close(tmp_fp) utils.downloadPage(SourceURI, tmp_path).addCallbacks(gotPage, gotError, None, None, [SourceURI], None) transfer_id = 0 # FIXME return {'TransferID': transfer_id} def upnp_CreateObject(self, *args, **kwargs): # print(f'CreateObject {kwargs}') if kwargs['ContainerID'] == 'DLNA.ORG_AnyContainer': if self.import_folder is not None: ContainerID = self.import_folder_id else: return failure.Failure(errorCode(712)) else: ContainerID = kwargs['ContainerID'] Elements = kwargs['Elements'] parent_item = self.get_by_id(ContainerID) if parent_item is None: return failure.Failure(errorCode(710)) if parent_item.item.restricted: return failure.Failure(errorCode(713)) if len(Elements) == 0: return failure.Failure(errorCode(712)) elt = DIDLElement.fromString(Elements) if elt.numItems() != 1: return failure.Failure(errorCode(712)) item = elt.getItems()[0] if item.parentID == 'DLNA.ORG_AnyContainer': item.parentID = ContainerID if (item.id != '' or item.parentID != ContainerID or item.restricted is True or item.title == ''): return failure.Failure(errorCode(712)) if '..' in item.title or '~' in item.title or os.sep in item.title: return failure.Failure(errorCode(712)) if item.upnp_class == 'object.container.storageFolder': if len(item.res) != 0: return failure.Failure(errorCode(712)) path = os.path.join(parent_item.get_path(), item.title) id = self.create('directory', path, parent_item) try: os.mkdir(path) except Exception: self.remove(id) return failure.Failure(errorCode(712)) if self.inotify is not None: mask = (IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_CHANGED) self.inotify.watch( path, mask=mask, autoAdd=False, callbacks=[partial(self.notify, parameter=id)], ) new_item = self.get_by_id(id) didl = DIDLElement() didl.addItem(new_item.item) return {'ObjectID': id, 'Result': didl.toString()} if item.upnp_class.startswith('object.item'): _, _, content_format, _ = item.res[0].protocolInfo.split(':') extension = mimetypes.guess_extension(content_format, strict=False) path = os.path.join(parent_item.get_realpath(), item.title + extension) id = self.create('item', path, parent_item) new_item = self.get_by_id(id) for res in new_item.item.res: res.importUri = new_item.url + '?import' res.data = None didl = DIDLElement() didl.addItem(new_item.item) return {'ObjectID': id, 'Result': didl.toString()} return failure.Failure(errorCode(712)) def hidden_upnp_DestroyObject(self, *args, **kwargs): ObjectID = kwargs['ObjectID'] item = self.get_by_id(ObjectID) if item is None: return failure.Failure(errorCode(701)) self.info(f'upnp_DestroyObject: {item.location}') try: item.location.remove() except Exception as msg: self.error(f'upnp_DestroyObject [{Exception}]: {msg}') return failure.Failure(errorCode(715)) return {}
def __init__(self, server, **kwargs): BackendStore.__init__(self, server, **kwargs) self.next_id = 1000 self.name = kwargs.get('name', 'my media') self.content = kwargs.get('content', None) if self.content != None: if isinstance(self.content, str): self.content = [self.content] l = [] for a in self.content: l += a.split(',') self.content = l else: self.content = xdg_content() self.content = [x[0] for x in self.content] if self.content == None: self.content = 'tests/content' if not isinstance(self.content, list): self.content = [self.content] self.content = Set([os.path.abspath(x) for x in self.content]) ignore_patterns = kwargs.get('ignore_patterns', []) self.store = {} self.inotify = None if kwargs.get('enable_inotify', 'yes') == 'yes': if INotify: try: self.inotify = INotify() except Exception as msg: self.info("%s", msg) else: self.info("%s", no_inotify_reason) else: self.info("FSStore content auto-update disabled upon user request") if kwargs.get('enable_destroy', 'no') == 'yes': self.upnp_DestroyObject = self.hidden_upnp_DestroyObject self.import_folder = kwargs.get('import_folder', None) if self.import_folder != None: self.import_folder = os.path.abspath(self.import_folder) if not os.path.isdir(self.import_folder): self.import_folder = None self.ignore_file_pattern = re.compile('|'.join(['^\..*'] + list(ignore_patterns))) parent = None self.update_id = 0 if (len(self.content) > 1 or utils.means_true(kwargs.get('create_root', False)) or self.import_folder != None): UPnPClass = classChooser('root') id = str(self.getnextID()) parent = self.store[id] = FSItem(id, parent, 'media', 'root', self.urlbase, UPnPClass, update=True, store=self) if self.import_folder != None: id = str(self.getnextID()) self.store[id] = FSItem(id, parent, self.import_folder, 'directory', self.urlbase, UPnPClass, update=True, store=self) self.import_folder_id = id for path in self.content: if isinstance(path, (list, tuple)): path = path[0] if self.ignore_file_pattern.match(path): continue try: path = path.encode('utf-8') # patch for #267 self.walk(path, parent, self.ignore_file_pattern) except Exception as msg: self.warning('on walk of %r: %r', path, msg) import traceback self.debug(traceback.format_exc()) self.wmc_mapping.update({'14': '0', '15': '0', '16': '0', '17': '0'}) louie.send('Coherence.UPnP.Backend.init_completed', None, backend=self)
class FSStore(BackendStore): logCategory = 'fs_store' implements = ['MediaServer'] description = """MediaServer exporting files from the file-system""" options = [{ 'option': 'name', 'type': 'string', 'default': 'my media', 'help': 'the name under this MediaServer shall show up with on other UPnP clients' }, { 'option': 'version', 'type': 'int', 'default': 2, 'enum': (2, 1), 'help': 'the highest UPnP version this MediaServer shall support', 'level': 'advance' }, { 'option': 'uuid', 'type': 'string', 'help': 'the unique (UPnP) identifier for this MediaServer, usually automatically set', 'level': 'advance' }, { 'option': 'content', 'type': 'string', 'default': xdg_content(), 'help': 'the path(s) this MediaServer shall export' }, { 'option': 'ignore_patterns', 'type': 'string', 'help': 'list of regex patterns, matching filenames will be ignored' }, { 'option': 'enable_inotify', 'type': 'string', 'default': 'yes', 'help': 'enable real-time monitoring of the content folders' }, { 'option': 'enable_destroy', 'type': 'string', 'default': 'no', 'help': 'enable deleting a file via an UPnP method' }, { 'option': 'import_folder', 'type': 'string', 'help': 'The path to store files imported via an UPnP method, if empty the Import method is disabled' }] def __init__(self, server, **kwargs): BackendStore.__init__(self, server, **kwargs) self.next_id = 1000 self.name = kwargs.get('name', 'my media') self.content = kwargs.get('content', None) if self.content != None: if isinstance(self.content, str): self.content = [self.content] l = [] for a in self.content: l += a.split(',') self.content = l else: self.content = xdg_content() self.content = [x[0] for x in self.content] if self.content == None: self.content = 'tests/content' if not isinstance(self.content, list): self.content = [self.content] self.content = Set([os.path.abspath(x) for x in self.content]) ignore_patterns = kwargs.get('ignore_patterns', []) self.store = {} self.inotify = None if kwargs.get('enable_inotify', 'yes') == 'yes': if INotify: try: self.inotify = INotify() except Exception as msg: self.info("%s", msg) else: self.info("%s", no_inotify_reason) else: self.info("FSStore content auto-update disabled upon user request") if kwargs.get('enable_destroy', 'no') == 'yes': self.upnp_DestroyObject = self.hidden_upnp_DestroyObject self.import_folder = kwargs.get('import_folder', None) if self.import_folder != None: self.import_folder = os.path.abspath(self.import_folder) if not os.path.isdir(self.import_folder): self.import_folder = None self.ignore_file_pattern = re.compile('|'.join(['^\..*'] + list(ignore_patterns))) parent = None self.update_id = 0 if (len(self.content) > 1 or utils.means_true(kwargs.get('create_root', False)) or self.import_folder != None): UPnPClass = classChooser('root') id = str(self.getnextID()) parent = self.store[id] = FSItem(id, parent, 'media', 'root', self.urlbase, UPnPClass, update=True, store=self) if self.import_folder != None: id = str(self.getnextID()) self.store[id] = FSItem(id, parent, self.import_folder, 'directory', self.urlbase, UPnPClass, update=True, store=self) self.import_folder_id = id for path in self.content: if isinstance(path, (list, tuple)): path = path[0] if self.ignore_file_pattern.match(path): continue try: path = path.encode('utf-8') # patch for #267 self.walk(path, parent, self.ignore_file_pattern) except Exception as msg: self.warning('on walk of %r: %r', path, msg) import traceback self.debug(traceback.format_exc()) self.wmc_mapping.update({'14': '0', '15': '0', '16': '0', '17': '0'}) louie.send('Coherence.UPnP.Backend.init_completed', None, backend=self) def __repr__(self): return str(self.__class__).split('.')[-1] def release(self): if self.inotify != None: self.inotify.release() def len(self): return len(self.store) def get_by_id(self, id): #print "get_by_id", id, type(id) # we have referenced ids here when we are in WMC mapping mode if isinstance(id, str): id = id.split('@', 1) id = id[0] elif isinstance(id, int): id = str(id) #try: # id = int(id) #except ValueError: # id = 1000 if id == '0': id = '1000' #print "get_by_id 2", id try: r = self.store[id] except: r = None #print "get_by_id 3", r return r def get_id_by_name(self, parent='0', name=''): self.info('get_id_by_name %r (%r) %r', parent, type(parent), name) try: parent = self.store[parent] self.debug("%r %d", parent, len(parent.children)) for child in parent.children: #if not isinstance(name, unicode): # name = name.decode("utf8") self.debug("%r %r %r", child.get_name(), child.get_realpath(), name == child.get_realpath()) if name == child.get_realpath(): return child.id except: import traceback self.info(traceback.format_exc()) self.debug('get_id_by_name not found') return None def get_url_by_name(self, parent='0', name=''): self.info('get_url_by_name %r %r', parent, name) id = self.get_id_by_name(parent, name) #print 'get_url_by_name', id if id == None: return '' return self.store[id].url def update_config(self, **kwargs): print("update_config", kwargs) if 'content' in kwargs: new_content = kwargs['content'] new_content = Set( [os.path.abspath(x) for x in new_content.split(',')]) new_folders = new_content.difference(self.content) obsolete_folders = self.content.difference(new_content) print(new_folders, obsolete_folders) for folder in obsolete_folders: self.remove_content_folder(folder) for folder in new_folders: self.add_content_folder(folder) self.content = new_content def add_content_folder(self, path): path = os.path.abspath(path) if path not in self.content: self.content.add(path) self.walk(path, self.store['1000'], self.ignore_file_pattern) def remove_content_folder(self, path): path = os.path.abspath(path) if path in self.content: id = self.get_id_by_name('1000', path) self.remove(id) self.content.remove(path) def walk(self, path, parent=None, ignore_file_pattern=''): self.debug("walk %r", path) containers = [] parent = self.append(path, parent) if parent != None: containers.append(parent) while len(containers) > 0: container = containers.pop() try: self.debug('adding %r', container.location) for child in container.location.children(): if ignore_file_pattern.match(child.basename()) != None: continue new_container = self.append(child.path, container) if new_container != None: containers.append(new_container) except UnicodeDecodeError: self.warning( "UnicodeDecodeError - there is something wrong with a file located in %r", container.get_path()) def create(self, mimetype, path, parent): self.debug("create %s %s %s %s", mimetype, path, type(path), parent) UPnPClass = classChooser(mimetype) if UPnPClass == None: return None id = self.getnextID() if mimetype in ('root', 'directory'): id = str(id) else: _, ext = os.path.splitext(path) id = str(id) + ext.lower() update = False if hasattr(self, 'update_id'): update = True self.store[id] = FSItem(id, parent, path, mimetype, self.urlbase, UPnPClass, update=True, store=self) if hasattr(self, 'update_id'): self.update_id += 1 #print self.update_id if self.server: if hasattr(self.server, 'content_directory_server'): self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) if parent is not None: value = (parent.get_id(), parent.get_update_id()) if self.server: if hasattr(self.server, 'content_directory_server'): self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value) return id def append(self, path, parent): self.debug("append %s %s %s", path, type(path), parent) if os.path.exists(path) == False: self.warning("path %r not available - ignored", path) return None if stat.S_ISFIFO(os.stat(path).st_mode): self.warning("path %r is a FIFO - ignored", path) return None try: mimetype, _ = mimetypes.guess_type(path, strict=False) if mimetype == None: if os.path.isdir(path): mimetype = 'directory' if mimetype == None: return None id = self.create(mimetype, path, parent) if mimetype == 'directory': if self.inotify is not None: mask = IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_CHANGED self.inotify.watch( path, mask=mask, autoAdd=False, callbacks=[partial(self.notify, parameter=id)]) return self.store[id] except OSError as msg: """ seems we have some permissions issues along the content path """ self.warning("path %r isn't accessible, error %r", path, msg) return None def remove(self, id): print('FSSTore remove id', id) try: item = self.store[id] parent = item.get_parent() item.remove() del self.store[id] if hasattr(self, 'update_id'): self.update_id += 1 if self.server: self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) #value = '%d,%d' % (parent.get_id(),parent_get_update_id()) value = (parent.get_id(), parent.get_update_id()) if self.server: self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value) except: pass def notify(self, ignore, path, mask, parameter=None): self.info("Event %s on %s - parameter %r", ', '.join(self.inotify.flag_to_human(mask)), path.path, parameter) if mask & IN_CHANGED: # FIXME react maybe on access right changes, loss of read rights? #print '%s was changed, parent %d (%s)' % (path, parameter, iwp.path) pass if (mask & IN_DELETE or mask & IN_MOVED_FROM): self.info('%s was deleted, parent %r (%s)', path.path, parameter, path.parent.path) id = self.get_id_by_name(parameter, path.path) if id != None: self.remove(id) if (mask & IN_CREATE or mask & IN_MOVED_TO): if mask & IN_ISDIR: self.info('directory %s was created, parent %r (%s)', path.path, parameter, path.parent.path) else: self.info('file %s was created, parent %r (%s)', path.path, parameter, path.parent.path) if self.get_id_by_name(parameter, path.path) is None: if path.isdir(): self.walk(path.path, self.get_by_id(parameter), self.ignore_file_pattern) else: if self.ignore_file_pattern.match(filename) == None: self.append(path.path, self.get_by_id(parameter)) def getnextID(self): ret = self.next_id self.next_id += 1 return ret def backend_import(self, item, data): try: f = open(item.get_path(), 'w+b') if hasattr(data, 'read'): data = data.read() f.write(data) f.close() item.rebuild(self.urlbase) return 200 except IOError: self.warning("import of file %s failed", item.get_path()) except Exception as msg: import traceback self.warning(traceback.format_exc()) return 500 def upnp_init(self): self.current_connection_id = None if self.server: self.server.connection_manager_server.set_variable( 0, 'SourceProtocolInfo', [ #'http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=01700000000000000000000000000000', #'http-get:*:audio/x-ms-wma:DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=11;DLNA.ORG_FLAGS=01700000000000000000000000000000', #'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', #'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', #'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', #'http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=00f00000000000000000000000000000', #'http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_PS_PAL;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000', #'http-get:*:video/x-ms-wmv:DLNA.ORG_PN=WMVMED_BASE;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000', 'internal:%s:audio/mpeg:*' % self.server.coherence.hostname, 'http-get:*:audio/mpeg:*', 'internal:%s:video/mp4:*' % self.server.coherence.hostname, 'http-get:*:video/mp4:*', 'internal:%s:application/ogg:*' % self.server.coherence.hostname, 'http-get:*:application/ogg:*', 'internal:%s:video/x-msvideo:*' % self.server.coherence.hostname, 'http-get:*:video/x-msvideo:*', 'internal:%s:video/mpeg:*' % self.server.coherence.hostname, 'http-get:*:video/mpeg:*', 'internal:%s:video/avi:*' % self.server.coherence.hostname, 'http-get:*:video/avi:*', 'internal:%s:video/divx:*' % self.server.coherence.hostname, 'http-get:*:video/divx:*', 'internal:%s:video/quicktime:*' % self.server.coherence.hostname, 'http-get:*:video/quicktime:*', 'internal:%s:image/gif:*' % self.server.coherence.hostname, 'http-get:*:image/gif:*', 'internal:%s:image/jpeg:*' % self.server.coherence.hostname, 'http-get:*:image/jpeg:*' ], default=True) self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) #self.server.content_directory_server.set_variable(0, 'SortCapabilities', '*') def upnp_ImportResource(self, *args, **kwargs): SourceURI = kwargs['SourceURI'] DestinationURI = kwargs['DestinationURI'] if DestinationURI.endswith('?import'): id = DestinationURI.split('/')[-1] id = id[:-7] # remove the ?import else: return failure.Failure(errorCode(718)) item = self.get_by_id(id) if item == None: return failure.Failure(errorCode(718)) def gotPage(headers): #print "gotPage", headers content_type = headers.get('content-type', []) if not isinstance(content_type, list): content_type = list(content_type) if len(content_type) > 0: extension = mimetypes.guess_extension(content_type[0], strict=False) item.set_path(None, extension) shutil.move(tmp_path, item.get_path()) item.rebuild(self.urlbase) if hasattr(self, 'update_id'): self.update_id += 1 if self.server: if hasattr(self.server, 'content_directory_server'): self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) if item.parent is not None: value = (item.parent.get_id(), item.parent.get_update_id()) if self.server: if hasattr(self.server, 'content_directory_server'): self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value) def gotError(error, url): self.warning("error requesting %s", url) self.info(error) os.unlink(tmp_path) return failure.Failure(errorCode(718)) tmp_fp, tmp_path = tempfile.mkstemp() os.close(tmp_fp) utils.downloadPage(SourceURI, tmp_path).addCallbacks(gotPage, gotError, None, None, [SourceURI], None) transfer_id = 0 # FIXME return {'TransferID': transfer_id} def upnp_CreateObject(self, *args, **kwargs): #print "CreateObject", kwargs if kwargs['ContainerID'] == 'DLNA.ORG_AnyContainer': if self.import_folder != None: ContainerID = self.import_folder_id else: return failure.Failure(errorCode(712)) else: ContainerID = kwargs['ContainerID'] Elements = kwargs['Elements'] parent_item = self.get_by_id(ContainerID) if parent_item == None: return failure.Failure(errorCode(710)) if parent_item.item.restricted: return failure.Failure(errorCode(713)) if len(Elements) == 0: return failure.Failure(errorCode(712)) elt = DIDLElement.fromString(Elements) if elt.numItems() != 1: return failure.Failure(errorCode(712)) item = elt.getItems()[0] if item.parentID == 'DLNA.ORG_AnyContainer': item.parentID = ContainerID if (item.id != '' or item.parentID != ContainerID or item.restricted == True or item.title == ''): return failure.Failure(errorCode(712)) if ('..' in item.title or '~' in item.title or os.sep in item.title): return failure.Failure(errorCode(712)) if item.upnp_class == 'object.container.storageFolder': if len(item.res) != 0: return failure.Failure(errorCode(712)) path = os.path.join(parent_item.get_path(), item.title) id = self.create('directory', path, parent_item) try: os.mkdir(path) except: self.remove(id) return failure.Failure(errorCode(712)) if self.inotify is not None: mask = IN_CREATE | IN_DELETE | IN_MOVED_FROM | IN_MOVED_TO | IN_CHANGED self.inotify.watch( path, mask=mask, autoAdd=False, callbacks=[partial(self.notify, parameter=id)]) new_item = self.get_by_id(id) didl = DIDLElement() didl.addItem(new_item.item) return {'ObjectID': id, 'Result': didl.toString()} if item.upnp_class.startswith('object.item'): _, _, content_format, _ = item.res[0].protocolInfo.split(':') extension = mimetypes.guess_extension(content_format, strict=False) path = os.path.join(parent_item.get_realpath(), item.title + extension) id = self.create('item', path, parent_item) new_item = self.get_by_id(id) for res in new_item.item.res: res.importUri = new_item.url + '?import' res.data = None didl = DIDLElement() didl.addItem(new_item.item) return {'ObjectID': id, 'Result': didl.toString()} return failure.Failure(errorCode(712)) def hidden_upnp_DestroyObject(self, *args, **kwargs): ObjectID = kwargs['ObjectID'] item = self.get_by_id(ObjectID) if item == None: return failure.Failure(errorCode(701)) print("upnp_DestroyObject", item.location) try: item.location.remove() except Exception as msg: print(Exception, msg) return failure.Failure(errorCode(715)) return {}