Coverage for coherence/backends/fs_storage.py : 43%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
# -*- coding: utf-8 -*-
# Licensed under the MIT license # http://opensource.org/licenses/mit-license.php
# Copyright 2006, Frank Scholz <coherence@beebits.net> FSStore - Filesystem media server
FSStore exposes media files found in the directory trees defined by the 'content' configuration entry.
The first ".jpg" or ".png" file found inside a media directory is served as a cover image.
The plugin is configured with:
<plugin active="yes"> <!-- The plugin identifier, mandatory --> <backend>FSStore</backend> <!-- A comma-separated list of path containing the medias to serve --> <content>/media/path1,/media/path2</content> <!-- The avertized media server name, default: "my media" --> <name>my media</name> <!-- The highest UPnP version this media server should support, default: 2 --> <version>2</version> <!-- A unique identifier used to reference the media server, autogenerated if not set explicitely. In this case, some control points might memorize it between runs and display the same media server more than once. --> <uuid>2f7f4096-cba3-4390-be7d-d1d07106a6f4</uuid> </plugin> """
INotify, IN_CREATE, IN_DELETE, IN_MOVED_FROM, IN_MOVED_TO, IN_ISDIR, IN_CHANGED) except Exception as msg: INotify = None no_inotify_reason = msg
# Sorting helpers
# strip the spaces NUMS.split(s)]
"""no thumbnail found"""
""" looks for a thumbnail file of the same basename in a folder named '.thumbs' relative to the file
returns the filename of the thumb, its mimetype and the correspondig DLNA PN string or throws an Exception otherwise """ name, ext = os.path.splitext(os.path.basename(filename)) pattern = os.path.join(os.path.dirname(filename), thumbnail_folder, name + '.*') for f in glob.glob(pattern): mimetype, _ = mimetypes.guess_type(f, strict=False) if mimetype in ('image/jpeg', 'image/png'): if mimetype == 'image/jpeg': dlna_pn = 'DLNA.ORG_PN=JPEG_TN' else: dlna_pn = 'DLNA.ORG_PN=PNG_TN' return os.path.abspath(f), mimetype, dlna_pn else: raise NoThumbnailFound()
update=False, store=None): else: path = os.path.join(parent.get_realpath(), str(self.id)) # self.location = FilePath(unicode(path)) urlbase += '/'
else:
# self.item.searchable = True # self.item.searchClass = 'object' self.location.isdir() is True): _, ext = os.path.splitext(self.cover) """ add the cover image extension to help clients not reacting on the mimetype """ self.item.albumArtURI = \ ''.join((urlbase, str(self.id), '?cover', str(ext))) else:
_, ext = os.path.splitext(parent.cover) """ add the cover image extension to help clients not reacting on the mimetype """ self.item.albumArtURI = \ ''.join((urlbase, str(self.id), '?cover', ext))
else:
except Exception: size = 0
self.store.server.coherence.config.get('transcoding', 'no') == 'yes'): if self.mimetype in ('application/ogg', 'audio/ogg', 'audio/x-wav', 'audio/x-m4a', 'application/x-flac'): new_res = Resource(self.url + '/transcoded.mp3', 'http-get:*:%s:*' % 'audio/mpeg') new_res.size = None # self.item.res.append(new_res)
'file://' + quote( self.get_path(), encoding='utf-8'), 'internal:%s:%s:*' % (host, self.mimetype))
else: res = Resource(self.url, 'http-get:*:*:*')
""" if this item is of type audio and we want to add a transcoding rule for it, this is the way to do it:
create a new Resource object, at least a 'http-get' and maybe an 'internal' one too
for transcoding to wav this looks like that
res = Resource( url_for_transcoded audio, 'http-get:*:audio/x-wav:%s'% ';'.join( ['DLNA.ORG_PN=JPEG_TN']+simple_dlna_tags)) res.size = None self.item.res.append(res) """
self.store.server.coherence.config.get( 'transcoding', 'no') == 'yes'): if self.mimetype in ('audio/mpeg', 'application/ogg', 'audio/ogg', 'audio/x-wav', 'audio/x-m4a', 'audio/flac', 'application/x-flac'): dlna_pn = 'DLNA.ORG_PN=LPCM' dlna_tags = simple_dlna_tags[:] # dlna_tags[1] = 'DLNA.ORG_OP=00' dlna_tags[2] = 'DLNA.ORG_CI=1' new_res = Resource( self.url + '?transcoded=lpcm', 'http-get:*:%s:%s' % ( 'audio/L16;rate=44100;channels=2', ';'.join([dlna_pn] + dlna_tags))) new_res.size = None # self.item.res.append(new_res)
if self.mimetype != 'audio/mpeg': new_res = Resource(self.url + '?transcoded=mp3', 'http-get:*:%s:*' % 'audio/mpeg') new_res.size = None # self.item.res.append(new_res)
""" if this item is an image and we want to add a thumbnail for it we have to follow these rules:
create a new Resource object, at least a 'http-get' and maybe an 'internal' one too
for an JPG this looks like that
res = Resource(url_for_thumbnail, 'http-get:*:image/jpg:%s'% ';'.join( ['DLNA.ORG_PN=JPEG_TN']+simple_dlna_tags)) res.size = size_of_thumbnail self.item.res.append(res)
and for a PNG the Resource creation is like that
res = Resource(url_for_thumbnail, 'http-get:*:image/png:%s'% ';'.join( simple_dlna_tags+['DLNA.ORG_PN=PNG_TN']))
if not hasattr(self.item, 'attachments'): self.item.attachments = {} self.item.attachments[key] = utils.StaticFile( filename_of_thumbnail) """
self.mimetype.startswith('video/')): try: filename, mimetype, dlna_pn = _find_thumbnail( self.get_path()) except NoThumbnailFound: pass except Exception: self.warning(traceback.format_exc()) else: dlna_tags = simple_dlna_tags[:] dlna_tags[ 3] = 'DLNA.ORG_FLAGS=00f00000000000000000000000000000'
hash_from_path = str(id(filename)) new_res = Resource( self.url + '?attachment=' + hash_from_path, 'http-get:*:%s:%s' % ( mimetype, ';'.join([dlna_pn] + dlna_tags))) new_res.size = os.path.getsize(filename) self.item.res.append(new_res) if not hasattr(self.item, 'attachments'): self.item.attachments = {} self.item.attachments[hash_from_path] = utils.StaticFile( filename)
# check for a subtitles file caption, _ = os.path.splitext(self.get_path()) caption = caption + '.srt' if os.path.exists(caption): hash_from_path = str(id(caption)) mimetype = 'smi/caption' new_res = Resource( self.url + '?attachment=' + hash_from_path, 'http-get:*:%s:%s' % (mimetype, '*')) new_res.size = os.path.getsize(caption) self.caption = new_res.data self.item.res.append(new_res) if not hasattr(self.item, 'attachments'): self.item.attachments = {} self.item.attachments[hash_from_path] = utils.StaticFile( caption)
# FIXME: getmtime is deprecated in Twisted 2.6 self.location.getmtime()) except Exception: self.item.date = None
# print "rebuild", self.mimetype if self.mimetype != 'item': return # print "rebuild for", self.get_path() mimetype, _ = mimetypes.guess_type(self.get_path(), strict=False) if mimetype is None: return self.mimetype = mimetype # print "rebuild", self.mimetype UPnPClass = classChooser(self.mimetype) self.item = UPnPClass(self.id, self.parent.id, self.get_name()) if getattr(self.parent, 'cover', None): _, ext = os.path.splitext(self.parent.cover) """ add the cover image extension to help clients not reacting on the mimetype """ self.item.albumArtURI = ''.join( (urlbase, str(self.id), '?cover', ext))
_, host_port, _, _, _ = urlsplit(urlbase) if host_port.find(':') != -1: host, port = tuple(host_port.split(':')) else: host = host_port
res = Resource( 'file://' + quote( self.get_path()), 'internal:%s:%s:*' % (host, self.mimetype)) try: res.size = self.location.getsize() except Exception: res.size = 0 self.item.res.append(res) res = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
try: res.size = self.location.getsize() except Exception: res.size = 0 self.item.res.append(res)
try: # FIXME: getmtime is deprecated in Twisted 2.6 self.item.date = datetime.fromtimestamp(self.location.getmtime()) except Exception: self.item.date = None
self.parent.update_id += 1
""" let's try to find in the current directory some jpg file, or png if the jpg search fails, and take the first one that comes around """ i.splitext()[1] in ('.jpg', '.JPG')] i.splitext()[1] in ('.png', '.PNG')] except UnicodeDecodeError: self.warning( "UnicodeDecodeError - " "there is something wrong with a file located in %r", self.location.path)
# print "FSItem remove", self.id, self.get_name(), self.parent if self.parent: self.parent.remove_child(self) del self.item
# print("remove_from %d (%s) child %d (%s)" % ( # self.id, self.get_name(), child.id, child.get_name())) if child in self.children: self.child_count -= 1 if isinstance(self.item, Container): self.item.childCount -= 1 self.children.remove(child) self.update_id += 1 self.sorted = False
else: return self.children[start:request_count]
else: return None
return None else: return self.location
if isinstance(self.location, FilePath): return self.location.path else: return self.location
if path is None: path = self.get_path() if extension is not None: path, old_ext = os.path.splitext(path) path = ''.join((path, extension)) if isinstance(self.location, FilePath): self.location = FilePath(path) else: self.location = path
else:
if self.cover: return self.cover try: return self.parent.cover except AttributeError: return None
return self.parent
return self.item.toString()
str(self.get_name().encode('ascii', 'xmlcharrefreplace'))
{'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'} ]
else: self.content = xdg_content() self.content = [x[0] for x in self.content] self.content = 'tests/content' self.content = [self.content]
if INotify: try: self.inotify = INotify() self.inotify.startReading() except Exception as msg: self.error("inotify disabled: %s", msg) self.inotify = None else: self.info("%s", no_inotify_reason) else:
self.upnp_DestroyObject = self.hidden_upnp_DestroyObject
self.import_folder = os.path.abspath(self.import_folder) if not os.path.isdir(self.import_folder): self.import_folder = None
r'|'.join([r'^\..*'] + list(ignore_patterns))) utils.means_true(kwargs.get('create_root', False)) or self.import_folder is not None): id, parent, 'media', 'root', self.urlbase, UPnPClass, update=True, store=self) except Exception as e: self.error('Error on setting self.stor[id}, Error on FSItem: ' '{}'.format(e)) exit(1)
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 path = str(path[0]) else: continue except Exception as msg: self.warning('on walk of %r: %r', path, msg) import traceback self.debug(traceback.format_exc())
'15': '0', '16': '0', '17': '0' })
self.inotify.stopReading()
# print "get_by_id", id, type(id) # we have referenced ids here when we are in WMC mapping mode id = id.decode('utf-8').split('@', 1)[0] # try: # id = int(id) # except ValueError: # id = 1000
# print "get_by_id 2", id # print "get_by_id 3", r
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 Exception as e: self.error('get_id_by_name: %r' % (e,)) import traceback self.info(traceback.format_exc()) self.debug('get_id_by_name not found')
return None
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 is None: return '' return self.store[id].url
self.info("update_config: {}".format(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('new folders: {}\nobsolete folders: {}'.format( 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
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)
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)
continue except UnicodeDecodeError: self.warning( "UnicodeDecodeError - " "there is something wrong with a file located in %r", container.get_path())
return None
else:
UPnPClass, update=True, store=self) # print self.update_id self.server.content_directory_server.set_variable( 0, 'SystemUpdateID', self.update_id) self.server.content_directory_server.set_variable( 0, 'ContainerUpdateIDs', value)
self.warning("path %r not available - ignored", path) return None
self.warning("path %r is a FIFO - ignored", path) return None
return 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)]) except OSError as os_msg: """ seems we have some permissions issues along the content path """ self.warning("path %r isn't accessible, error %r", path, os_msg)
self.debug('FSSTore remove id: {}'.format(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 KeyError: pass
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 is not 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(parameter) is None: self.append( str(path.path), str(self.get_by_id(parameter)))
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
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) 0, 'SystemUpdateID', self.update_id) # self.server.content_directory_server.set_variable( # 0, 'SortCapabilities', '*')
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("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}
# print "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))
ObjectID = kwargs['ObjectID']
item = self.get_by_id(ObjectID) if item is None: return failure.Failure(errorCode(701))
self.info("upnp_DestroyObject: {}".format(item.location)) try: item.location.remove() except Exception as msg: self.error('upnp_DestroyObject [{}]: {}'.format(Exception, msg)) return failure.Failure(errorCode(715))
return {}
from twisted.internet import reactor
p = 'tests/content' f = FSStore(None, name='my media', content=p, urlbase='http://localhost/xyz')
print(f.len()) print(f.get_by_id(1000).child_count, f.get_by_id(1000).get_xml()) print(f.get_by_id(1001).child_count, f.get_by_id(1001).get_xml()) print(f.get_by_id(1002).child_count, f.get_by_id(1002).get_xml()) print(f.get_by_id(1003).child_count, f.get_by_id(1003).get_xml()) print(f.get_by_id(1004).child_count, f.get_by_id(1004).get_xml()) print(f.get_by_id(1005).child_count, f.get_by_id(1005).get_xml()) print(f.store[1000].get_children(0, 0)) # print(f.upnp_Search( # ContainerID='4', # Filter='dc:title,upnp:artist', # RequestedCount='1000', # StartingIndex='0', # SearchCriteria='(upnp:class = "object.container.album.musicAlbum")', # SortCriteria='+dc:title'))
f.upnp_ImportResource(SourceURI='http://spiegel.de', DestinationURI='ttt')
reactor.run() |