# -*- 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 explicitly. 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>
'''
import glob
import mimetypes
import os
import re
import shutil
import stat
import tempfile
import traceback
from datetime import datetime
from functools import partial
from urllib.parse import quote

mimetypes.init()
mimetypes.add_type('audio/x-m4a', '.m4a')
mimetypes.add_type('audio/x-musepack', '.mpc')
mimetypes.add_type('audio/x-wavpack', '.wv')
mimetypes.add_type('video/mp4', '.mp4')
mimetypes.add_type('video/mpegts', '.ts')
mimetypes.add_type('video/divx', '.divx')
mimetypes.add_type('video/divx', '.avi')
mimetypes.add_type('video/x-matroska', '.mkv')

from urllib.parse import urlsplit

from twisted.python.filepath import FilePath
from twisted.python import failure

from coherence.upnp.core.DIDLLite import classChooser, Container, Resource
from coherence.upnp.core.DIDLLite import DIDLElement
from coherence.upnp.core.DIDLLite import simple_dlna_tags
from coherence.upnp.core.soap_service import errorCode

from coherence.upnp.core import utils

try:
    from twisted.internet.inotify import (
        INotify, IN_CREATE, IN_DELETE, IN_MOVED_FROM, IN_MOVED_TO,
        IN_ISDIR, IN_CHANGED, _FLAG_TO_HUMAN)
except Exception as msg:
    INotify = None
    no_inotify_reason = msg

from coherence.extern.xdg import xdg_content

from coherence.backend import BackendItem, BackendStore

# Sorting helpers
NUMS = re.compile('([0-9]+)')


def _natural_key(s):
    # strip the spaces
    s = s.get_name().strip()
    return [part.isdigit() and int(part) or part.lower() for part in
            NUMS.split(s)]


class NoThumbnailFound(Exception):
    '''no thumbnail found'''


def _find_thumbnail(filename, thumbnail_folder='.thumbs'):
    ''' 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()


class FSItem(BackendItem):
    logCategory = 'fs_item'

    def __init__(self, object_id, parent, path, mimetype, urlbase, UPnPClass,
                 update=False, store=None):
        BackendItem.__init__(self)
        self.id = object_id
        self.parent = parent
        if parent:
            parent.add_child(self, update=update)
        if mimetype == 'root':
            self.location = str(path)
        else:
            if mimetype == 'item' and path is None:
                path = os.path.join(parent.get_realpath(), str(self.id))
            # self.location = FilePath(unicode(path))
            self.location = FilePath(path)
        self.mimetype = mimetype
        if urlbase[-1] != '/':
            urlbase += '/'
        self.url = urlbase + str(self.id)

        self.store = store

        if parent is None:
            parent_id = -1
        else:
            parent_id = parent.get_id()

        self.item = UPnPClass(object_id, parent_id, self.get_name())
        if isinstance(self.item, Container):
            self.item.childCount = 0
        self.child_count = 0
        self.children = []
        self.sorted = False
        self.caption = None

        if mimetype in ['directory', 'root']:
            self.update_id = 0
            self.get_url = lambda: self.url
            # self.item.searchable = True
            # self.item.searchClass = 'object'
            if (isinstance(self.location, FilePath) and
                    self.location.isdir() is True):
                self.check_for_cover_art()
                if getattr(self, 'cover', None):
                    _, 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:
            self.get_url = lambda: self.url

            if self.mimetype.startswith('audio/'):
                if getattr(parent, 'cover', None):
                    _, 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))

            _, host_port, _, _, _ = urlsplit(urlbase)
            if host_port.find(':') != -1:
                host, port = tuple(host_port.split(':'))
            else:
                host = host_port

            try:
                size = self.location.getsize()
            except Exception:
                size = 0

            if (self.store.server and
                    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',
                                       f'http-get:*:{"audio/mpeg"}:*')
                    new_res.size = None
                    # self.item.res.append(new_res)

            if mimetype != 'item':
                res = Resource(
                    'file://' + quote(
                        self.get_path(),
                        encoding='utf-8'),
                    f'internal:{host}:{self.mimetype}:*')
                res.size = size
                self.item.res.append(res)

            if mimetype != 'item':
                res = Resource(self.url, f'http-get:*:{self.mimetype}:*')
            else:
                res = Resource(self.url, 'http-get:*:*:*')

            res.size = size
            self.item.res.append(res)

            ''' 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)
            '''

            if (self.store.server and
                    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',
                        f'http-get:*:{"audio/L16;rate=44100;channels=2"}:'
                        f'{";".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',
                                           f'http-get:*:{"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)
            '''

            if (self.mimetype in ('image/jpeg', 'image/png') or
                    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,
                        f'http-get:*:{mimetype}:'
                        f'{";".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)

            if self.mimetype.startswith('video/'):
                # 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,
                        f'http-get:*:{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)

            try:
                # FIXME: getmtime is deprecated in Twisted 2.6
                self.item.date = datetime.fromtimestamp(
                    self.location.getmtime())
            except Exception:
                self.item.date = None

    def rebuild(self, urlbase):
        # 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()),
            f'internal:{host}:{self.mimetype}:*')
        try:
            res.size = self.location.getsize()
        except Exception:
            res.size = 0
        self.item.res.append(res)
        res = Resource(self.url, f'http-get:*:{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

    def check_for_cover_art(self):
        ''' 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
        '''
        try:
            jpgs = [i.path for i in self.location.children() if
                    i.splitext()[1] in ('.jpg', '.JPG')]
            try:
                self.cover = jpgs[0]
            except IndexError:
                pngs = [i.path for i in self.location.children() if
                        i.splitext()[1] in ('.png', '.PNG')]
                try:
                    self.cover = pngs[0]
                except IndexError:
                    return
        except UnicodeDecodeError:
            self.warning(
                f'UnicodeDecodeError - there is something wrong with a '
                f'file located in {self.location.path}')

    def remove(self):
        # print('FSItem remove', self.id, self.get_name(), self.parent)
        if self.parent:
            self.parent.remove_child(self)
        del self.item

    def add_child(self, child, update=False):
        self.children.append(child)
        self.child_count += 1
        if isinstance(self.item, Container):
            self.item.childCount += 1
        if update:
            self.update_id += 1
        self.sorted = False

    def remove_child(self, child):
        # print(f'remove_from {self.id:d} ({self.get_name()}) '
        #       f'child {child.id:d} ({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

    def get_children(self, start=0, request_count=0):
        if not self.sorted:
            self.children.sort(key=_natural_key)
            self.sorted = True
        if request_count == 0:
            return self.children[start:]
        else:
            return self.children[start:request_count]

    def get_child_count(self):
        return self.child_count

    def get_id(self):
        return self.id

    def get_update_id(self):
        if hasattr(self, 'update_id'):
            return self.update_id
        else:
            return None

    def get_path(self):
        if self.mimetype in ['directory', 'root']:
            return None
        if isinstance(self.location, FilePath):
            return self.location.path
        else:
            return self.location

    def get_realpath(self):
        if isinstance(self.location, FilePath):
            return self.location.path
        else:
            return self.location

    def set_path(self, path=None, extension=None):
        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

    def get_name(self):
        if isinstance(self.location, FilePath):
            name = self.location.basename()
        else:
            name = self.location
        return name

    def get_cover(self):
        if self.cover:
            return self.cover
        try:
            return self.parent.cover
        except AttributeError:
            return None

    def get_parent(self):
        return self.parent

    def get_item(self):
        return self.item

    def get_xml(self):
        return self.item.toString()

    def __repr__(self):
        return 'id: ' + str(self.id) + ' @ ' + \
               str(self.get_name().encode('ascii', 'xmlcharrefreplace'))


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'},
        {'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(path[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
                 'http-get:*:application/ogg:*',
                 f'internal:{self.server.coherence.hostname}:video/x-msvideo:*',  # noqa
                 '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
                 '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 {}


if __name__ == '__main__':
    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()
