# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# Copyright 2008, Frank Scholz <coherence@beebits.net>

import mimetypes
import time

from lxml import etree

mimetypes.init()

try:
    import hashlib

    def md5(s):
        m = hashlib.md5()
        m.update(s)
        return m.hexdigest()

    def sha256(s):
        m = hashlib.sha256()
        m.update(s)
        return m.hexdigest()
except ImportError:
    import md5 as oldmd5

    def md5(s):
        m = oldmd5.new()
        m.update(s)
        return m.hexdigest()

from twisted.internet import reactor, defer
from twisted.python import failure

from coherence.upnp.core import DIDLLite
from coherence.upnp.core.soap_service import errorCode
from coherence.upnp.core import utils

from coherence.backend import BackendItem, BackendStore

ROOT_CONTAINER_ID = 0
AUDIO_CONTAINER = 100
AUDIO_ALL_CONTAINER_ID = 101
AUDIO_ARTIST_CONTAINER_ID = 102
AUDIO_ALBUM_CONTAINER_ID = 103
AUDIO_PLAYLIST_CONTAINER_ID = 104
AUDIO_GENRE_CONTAINER_ID = 105
AUDIO_TAG_CONTAINER_ID = 106

VIDEO_CONTAINER_ID = 200

from urllib.parse import urlsplit


class ProxySong(utils.ReverseProxyResource):

    def __init__(self, uri):
        self.uri = uri
        _, host_port, path, query, _ = urlsplit(uri)
        if host_port.find(':') != -1:
            host, port = tuple(host_port.split(':'))
            port = int(port)
        else:
            host = host_port
            port = 80

        utils.ReverseProxyResource.__init__(self, host, port,
                                            '?'.join((path, query)))


class Container(BackendItem):
    logCategory = 'ampache_store'

    get_path = None

    def __init__(self, id, parent_id, name, store=None, children_callback=None,
                 container_class=DIDLLite.Container, play_container=False):
        BackendItem.__init__(self)
        self.id = id
        self.parent_id = parent_id
        self.name = name
        self.mimetype = 'directory'
        self.container_class = container_class
        self.update_id = 0
        if children_callback is not None:
            self.children = children_callback
        else:
            self.children = []
        self.childCount = None

        self.store = store
        self.play_container = play_container

        if self.store is not None:
            self.get_url = lambda: self.store.urlbase + str(self.id)

    def add_child(self, child):
        self.children.append(child)
        if self.childCount is None:
            self.childCount = 0
        self.childCount += 1

    def get_children(self, start=0, end=0):
        self.info(f'container.get_children {start} {end}')
        if (end - start > 250 or
                end - start == 0):
            end = start + 250

        if callable(self.children):
            return self.children(start, end - start)
        else:
            children = self.children
        if end == 0:
            return children[start:]
        else:
            return children[start:end]

    def get_child_count(self):
        if self.childCount is None:
            if callable(self.children):
                self.childCount = len(self.children())
            else:
                self.childCount = len(self.children)
        return self.childCount

    def get_item(self):
        item = self.container_class(self.id, self.parent_id, self.name)
        item.childCount = self.get_child_count()
        # if self.store and self.play_container:
        #    if item.childCount > 0:
        #        d = defer.maybeDeferred(self.get_children, 0, 1)

        #        def process_result(r,item):
        #            res = DIDLLite.PlayContainerResource(
        #                self.store.server.uuid,
        #                cid=self.get_id(),fid=r[0].get_id())
        #            item.res.append(res)
        #            return item

        #        def got_error(f,item):
        #            return item

        #        d.addCallback(process_result,item)
        #        d.addErrback(got_error,item)
        #        return d
        return item

    def get_name(self):
        return self.name

    def get_id(self):
        return self.id


class Playlist(BackendItem):
    logCategory = 'ampache_store'

    get_path = None

    def __init__(self, store, element):
        BackendItem.__init__(self)
        self.store = store
        self.ampache_id = element.get('id')
        self.id = f'playlist.{int(element.get("id")):d}'
        self.title = element.find('name').text
        self.creator = element.find('owner').text
        self.tracks = int(element.find('items').text)
        try:
            self.cover = element.find('art').text
        except Exception:
            self.cover = None

    def get_children(self, start=0, end=0):
        return self.store.ampache_query('playlist_songs', start, end - start,
                                        filter=self.ampache_id)

    def get_child_count(self):
        return self.tracks

    def get_item(self, parent_id=AUDIO_PLAYLIST_CONTAINER_ID):
        item = DIDLLite.PlaylistItem(self.id, parent_id, self.title)
        item.childCount = self.get_child_count()
        # item.artist = self.artist
        item.albumArtURI = self.cover
        return item

    def get_id(self):
        return self.id

    def get_name(self):
        return self.title

    def get_cover(self):
        return self.cover


class Album(BackendItem):
    logCategory = 'ampache_store'

    get_path = None

    def __init__(self, store, element):
        BackendItem.__init__(self)
        self.store = store
        self.ampache_id = element.get('id')
        self.id = f'album.{int(element.get("id")):d}'
        self.title = element.find('name').text
        self.artist = element.find('artist').text
        self.tracks = int(element.find('tracks').text)
        try:
            self.cover = element.find('art').text
        except Exception:
            self.cover = None

    def get_children(self, start=0, end=0):
        return self.store.ampache_query('album_songs', start, end - start,
                                        filter=self.ampache_id)

    def get_child_count(self):
        return self.tracks

    def get_item(self, parent_id=AUDIO_ALBUM_CONTAINER_ID):
        item = DIDLLite.MusicAlbum(self.id, parent_id, self.title)
        item.childCount = self.get_child_count()
        item.artist = self.artist
        item.albumArtURI = self.cover
        # if item.childCount > 0:
        #    d = defer.maybeDeferred(self.get_children, 0, 1)

        #    def process_result(r,item):
        #        res = DIDLLite.PlayContainerResource(
        #            self.store.server.uuid,
        #            cid=self.get_id(),
        #            fid=r[0].get_id())
        #        item.res.append(res)
        #        return item

        #    def got_error(f,item):
        #        return item

        #    d.addCallback(process_result,item)
        #    d.addErrback(got_error,item)
        #    return d
        return item

    def get_id(self):
        return self.id

    def get_name(self):
        return self.title

    def get_cover(self):
        return self.cover


class Artist(BackendItem):
    logCategory = 'ampache_store'

    get_path = None

    def __init__(self, store, element):
        BackendItem.__init__(self)
        self.store = store
        self.ampache_id = element.get('id')
        self.id = f'artist.{int(element.get("id")):d}'

        try:
            self.count_albums = int(element.find('albums').text)
        except Exception:
            self.count_albums = None
        try:
            self.count_songs = int(element.find('songs').text)
        except Exception:
            self.count_songs = None
        self.name = element.find('name').text

    def get_children(self, start=0, end=0):
        return self.store.ampache_query('artist_albums', start, end - start,
                                        filter=self.ampache_id)

    def get_child_count(self):
        if self.count_albums is not None:
            return self.count_albums

        def got_childs(result):
            self.count_albums = len(result)
            return self.count_albums

        d = self.get_children()
        d.addCallback(got_childs)
        return d

    def get_item(self, parent_id=AUDIO_ARTIST_CONTAINER_ID):
        item = DIDLLite.MusicArtist(self.id, parent_id, self.name)
        return item

    def get_id(self):
        return self.id

    def get_name(self):
        return self.name


class Genre(BackendItem):
    logCategory = 'ampache_store'

    get_path = None

    def __init__(self, store, element):
        BackendItem.__init__(self)
        self.store = store
        self.ampache_id = element.get('id')
        self.id = f'genre.{int(element.get("id")):d}'

        try:
            self.count_albums = int(element.find('albums').text)
        except Exception:
            self.count_albums = None
        try:
            self.count_artists = int(element.find('artists').text)
        except Exception:
            self.count_artists = None
        try:
            self.count_songs = int(element.find('songs').text)
        except Exception:
            self.count_songs = None
        self.name = element.find('name').text

    def get_children(self, start=0, end=0):
        return self.store.ampache_query('genre_songs', start, end - start,
                                        filter=self.ampache_id)

    def get_child_count(self):
        if self.count_songs is not None:
            return self.count_songs

        def got_childs(result):
            self.count_songs = len(result)
            return self.count_songs

        d = self.get_children()
        d.addCallback(got_childs)
        return d

    def get_item(self, parent_id=AUDIO_GENRE_CONTAINER_ID):
        item = DIDLLite.Genre(self.id, parent_id, self.name)
        return item

    def get_id(self):
        return self.id

    def get_name(self):
        return self.name


class Tag(BackendItem):
    logCategory = 'ampache_store'

    get_path = None

    def __init__(self, store, element):
        BackendItem.__init__(self)
        self.store = store
        self.ampache_id = element.get('id')
        self.id = f'tag.{int(element.get("id")):d}'

        try:
            self.count_albums = int(element.find('albums').text)
        except Exception:
            self.count_albums = None
        try:
            self.count_artists = int(element.find('artists').text)
        except Exception:
            self.count_artists = None
        try:
            self.count_songs = int(element.find('songs').text)
        except Exception:
            self.count_songs = None
        self.name = element.find('name').text

    def get_children(self, start=0, end=0):
        return self.store.ampache_query('tag_songs', start, end - start,
                                        filter=self.ampache_id)

    def get_child_count(self):
        if self.count_songs is not None:
            return self.count_songs

        def got_childs(result):
            self.count_songs = len(result)
            return self.count_songs

        d = self.get_children()
        d.addCallback(got_childs)
        return d

    def get_item(self, parent_id=AUDIO_TAG_CONTAINER_ID):
        item = DIDLLite.Genre(self.id, parent_id, self.name)
        return item

    def get_id(self):
        return self.id

    def get_name(self):
        return self.name


class Track(BackendItem):
    logCategory = 'ampache_store'

    def __init__(self, store, element):
        BackendItem.__init__(self)
        self.store = store
        self.id = f'song.{int(element.get("id")):d}'
        self.parent_id = f'album.{int(element.find("album").get("id")):d}'

        self.url = element.find('url').text

        seconds = int(element.find('time').text)
        self.duration = time.strftime(
            '%H:%M:%S', time.gmtime(seconds))

        self.bitrate = 0

        self.title = element.find('title').text
        self.artist = element.find('artist').text
        self.album = element.find('album').text
        try:
            self.genre = element.find('genre').text
        except Exception:
            self.genre = None
        self.track_nr = element.find('track').text

        try:
            self.cover = element.find('art').text
        except Exception:
            self.cover = None

        self.mimetype = None
        try:
            self.mimetype = element.find('mime').text
        except Exception:
            self.mimetype, _ = mimetypes.guess_type(self.url, strict=False)
        if self.mimetype is None:
            self.mimetype = "audio/mpeg"
        try:
            self.size = int(element.find('size').text)
        except Exception:
            self.size = 0

        if self.store.proxy:
            self.location = ProxySong(self.url)

    def get_children(self, start=0, request_count=0):
        return []

    def get_child_count(self):
        return 0

    def get_item(self, parent_id=None):

        self.debug(f'Track get_item {self.id} @ {self.parent_id}')

        # create item
        item = DIDLLite.MusicTrack(self.id, self.parent_id)
        item.album = self.album

        item.artist = self.artist
        # item.date =
        item.genre = self.genre
        item.originalTrackNumber = self.track_nr
        item.title = self.title

        item.albumArtURI = self.cover

        # add http resource
        res = DIDLLite.Resource(self.get_url(),
                                f'http-get:*:{self.mimetype}:*')
        if self.size > 0:
            res.size = self.size
        if self.duration > 0:
            res.duration = str(self.duration)
        if self.bitrate > 0:
            res.bitrate = str(self.bitrate)
        item.res.append(res)

        return item

    def get_id(self):
        return self.id

    def get_name(self):
        return self.title

    def get_url(self):
        if self.store.proxy:
            return self.store.urlbase + str(self.id)
        else:
            return self.url

    def get_path(self):
        return None


class Video(BackendItem):
    logCategory = 'ampache_store'

    def __init__(self, store, element):
        BackendItem.__init__(self)
        self.store = store
        self.id = f'video.{int(element.get("id")):d}'

        self.url = element.find('url').text

        try:
            seconds = int(element.find('time').text)
            hours = seconds / 3600
            seconds = seconds - hours * 3600
            minutes = seconds / 60
            seconds = seconds - minutes * 60
            self.duration = f'{hours:d}:{minutes:02d}:{seconds:02d}'
        except Exception:
            self.duration = 0

        self.cover = None

        self.title = element.find('title').text

        self.mimetype = None
        try:
            self.mimetype = element.find('mime').text
        except Exception:
            self.mimetype, _ = mimetypes.guess_type(self.url, strict=False)
        if self.mimetype is None:
            self.mimetype = 'video/avi'
        try:
            self.size = int(element.find('size').text)
        except Exception:
            self.size = 0

        if self.store.proxy:
            self.location = ProxySong(self.url)

    def get_children(self, start=0, request_count=0):
        return []

    def get_child_count(self):
        return 0

    def get_item(self, parent_id=VIDEO_CONTAINER_ID):

        self.debug(f'video get_item {self.id} @ {parent_id}')

        # create item
        item = DIDLLite.VideoItem(self.id, parent_id)
        item.title = self.title

        item.albumArtURI = self.cover

        # add http resource
        res = DIDLLite.Resource(self.get_url(),
                                f'http-get:*:{self.mimetype}:*')
        if self.size > 0:
            res.size = self.size
        if self.duration > 0:
            res.duration = str(self.duration)
        item.res.append(res)

        return item

    def get_id(self):
        return self.id

    def get_name(self):
        return self.title

    def get_url(self):
        if self.store.proxy:
            return self.store.urlbase + str(self.id)
        else:
            return self.url

    def get_path(self):
        return None


class AmpacheStore(BackendStore):
    '''This is a backend to the Ampache Media DB

    .. versionchanged:: 0.9.0
        Migrated from louie/dispatcher to EventDispatcher
    '''

    implements = ['MediaServer']
    logCategory = 'ampache_store'

    def __init__(self, server, **kwargs):
        BackendStore.__init__(self, server, **kwargs)
        self.config = kwargs
        self.name = kwargs.get('name', 'Ampache')
        self.key = kwargs.get('password', kwargs.get('key', ''))
        self.user = kwargs.get('user', None)
        self.url = kwargs.get('url',
                              'http://localhost/ampache/server/xml.server.php')

        if kwargs.get('proxy', 'no') in [1, 'Yes', 'yes', 'True', 'true']:
            self.proxy = True
        else:
            self.proxy = False

        self.update_id = 0
        self.token = None

        self.songs = 0
        self.albums = 0
        self.artists = 0

        self.api_version = int(kwargs.get('api_version', 350001))
        # self.api_version=int(kwargs.get('api_version',340001))

        self.get_token()

    def __repr__(self):
        return 'Ampache storage'

    def get_by_id(self, id):
        self.info(f'looking for id {id}')
        if isinstance(id, str):
            id = id.split('@', 1)[0]
        elif isinstance(id, bytes):
            id = id.decode('utf-8').split('@', 1)[0]
        if isinstance(id, str) and id.startswith('artist_all_tracks_'):
            try:
                return self.containers[id]
            except Exception:
                return None
        item = None
        try:
            id = int(id)
            item = self.containers[id]
        except ValueError:
            try:
                type, id = id.split('.')
                if type in ['song', 'artist', 'album', 'playlist', 'genre',
                            'tag', 'video']:
                    item = self.ampache_query(type, filter=str(id))
            except ValueError:
                return None
        return item

    def got_auth_response(self, response, renegotiate=False):
        self.info(f'got_auth_response {response}')
        try:
            response = etree.fromstring(response)
        except SyntaxError as msg:
            self.warning(f'error parsing ampache answer {msg}')
            raise SyntaxError(f'error parsing ampache answer {msg}')
        try:
            error = response.find('error').text
            self.warning(f'error on token request {error}')
            raise ValueError(error)
        except AttributeError:
            try:
                self.token = response.find('auth').text
                self.songs = int(response.find('songs').text)
                self.albums = int(response.find('albums').text)
                self.artists = int(response.find('artists').text)
                try:
                    self.playlists = int(response.find('playlists').text)
                except Exception:
                    self.playlists = 0
                try:
                    self.genres = int(response.find('genres').text)
                except Exception:
                    self.genres = 0
                try:
                    self.tags = int(response.find('tags').text)
                except Exception:
                    self.tags = 0
                try:
                    self.videos = int(response.find('videos').text)
                except Exception:
                    self.videos = 0
                self.info('ampache returned auth token %r', self.token)
                self.info(
                    f'Songs: {self.songs:d}, Artists: {self.artists:d}, '
                    f'Albums: {self.albums:d}, Playlists {self.playlists:d}, '
                    f'Genres {self.genres:d}, Tags {self.tag:d}, '
                    f'Videos {self.videos:d}')

                if not renegotiate:
                    self.containers = {}
                    self.containers[ROOT_CONTAINER_ID] = \
                        Container(ROOT_CONTAINER_ID, -1, self.name, store=self)

                    self.wmc_mapping.update(
                        {'4': lambda: self.get_by_id(
                            AUDIO_ALL_CONTAINER_ID),
                         # all tracks
                         '5': lambda: self.get_by_id(
                             AUDIO_GENRE_CONTAINER_ID),
                         # all genres
                         '6': lambda: self.get_by_id(
                             AUDIO_ARTIST_CONTAINER_ID),
                         # all artists
                         '7': lambda: self.get_by_id(
                             AUDIO_ALBUM_CONTAINER_ID),
                         # all albums
                         '13': lambda: self.get_by_id(
                             AUDIO_PLAYLIST_CONTAINER_ID),  # all playlists
                         '8': lambda: self.get_by_id(
                             VIDEO_CONTAINER_ID),
                         # all videos
                         })

                    self.init_completed = True
            except AttributeError:
                raise ValueError('no authorization token returned')

    def got_auth_error(self, e, renegotiate=False):
        self.warning(f'error calling ampache {e}')
        if not renegotiate:
            self.on_init_failed(msg=e)

    def get_token(self, renegotiate=False):
        '''Ask Ampache for the authorization token'''
        timestamp = int(time.time())
        if self.api_version < 350001:
            passphrase = md5(f'{timestamp:d}{self.key}')
        else:
            passphrase = sha256(f'{timestamp:d}{sha256(self.key)}')
        request = ''.join(
            (self.url,
             f'?action=handshake&auth={passphrase}&timestamp={timestamp:d}'))
        if self.user is not None:
            request = ''.join((request, f'&user={self.user}'))
        if self.api_version is not None:
            request = ''.join((request, f'&version={str(self.api_version)}'))
        self.info(f'auth_request {request}')
        d = utils.getPage(request)
        d.addCallback(self.got_auth_response, renegotiate)
        d.addErrback(self.got_auth_error, renegotiate)
        return d

    def got_error(self, e):
        self.warning(f'error calling ampache {e}')
        return e

    def got_response(self, response, query_item, request):
        self.info(f'got a response for {query_item}')
        self.debug(response)
        response = etree.fromstring(response)
        items = []
        try:
            error = response.find('error')
            self.warning(
                f'error on token request {error.attrib["code"]} {error.text}')
            if error.attrib['code'] == '401':
                # session error, we need to renegotiate our session
                d = self.get_token(renegotiate=True)

                def resend_request(result, old_request):
                    # exchange the auth token in the resending request
                    new_request = old_request.split('&')
                    for part in new_request:
                        if part.startswith('auth='):
                            new_request[new_request.index(
                                part)] = f'auth={self.token}'
                            break
                    new_request = '&'.join(new_request)
                    self.info(f'ampache_query {new_request}')
                    return utils.getPage(new_request)

                d.addCallback(resend_request, request)
                d.addErrBack(self.got_error)
                return d
            raise ValueError(error.text)
        except AttributeError:
            if query_item in ('song', 'artist', 'album',
                              'playlist', 'genre', 'tag', 'video'):
                q = response.find(query_item)
                if q is None:
                    return None
                else:
                    if q.tag in ['song']:
                        return Track(self, q)
                    if q.tag == 'artist':
                        return Artist(self, q)
                    if q.tag in ['album']:
                        return Album(self, q)
                    if q.tag in ['playlist']:
                        return Playlist(self, q)
                    if q.tag in ['genre']:
                        return Genre(self, q)
                    if q.tag in ['tag']:
                        return Tag(self, q)
                    if q.tag in ['video']:
                        return Video(self, q)
            else:
                if query_item in ('songs', 'artists', 'albums', 'playlists',
                                  'genres', 'tags', 'videos'):
                    query_item = query_item[:-1]
                if query_item in ('playlist_songs', 'album_songs',
                                  'genre_songs', 'tag_songs'):
                    query_item = 'song'
                if query_item in ('artist_albums',):
                    query_item = 'album'
                for q in response.findall(query_item):
                    if query_item in ('song',):
                        items.append(Track(self, q))
                    if query_item in ('artist',):
                        items.append(Artist(self, q))
                    if query_item in ('album',):
                        items.append(Album(self, q))
                    if query_item in ('playlist',):
                        items.append(Playlist(self, q))
                    if query_item in ('genre',):
                        items.append(Genre(self, q))
                    if query_item in ('tag',):
                        items.append(Tag(self, q))
                    if query_item in ('video',):
                        items.append(Video(self, q))
        return items

    def ampache_query(self, item, start=0, request_count=0, filter=None):
        request = ''.join(
            (self.url, f'?action={item}&auth={self.token}&offset={start:d}'))
        if request_count > 0:
            request = ''.join((request, f'&limit={request_count:d}'))
        if filter is not None:
            request = ''.join((request, f'&filter={filter}'))
        self.info(f'ampache_query {request}')
        d = utils.getPage(request)
        d.addCallback(self.got_response, item, request)
        d.addErrback(self.got_error)
        return d

    def ampache_query_songs(self, start=0, request_count=0):
        return self.ampache_query('songs', start, request_count)

    def ampache_query_albums(self, start=0, request_count=0):
        return self.ampache_query('albums', start, request_count)

    def ampache_query_artists(self, start=0, request_count=0):
        return self.ampache_query('artists', start, request_count)

    def ampache_query_playlists(self, start=0, request_count=0):
        return self.ampache_query('playlists', start, request_count)

    def ampache_query_genres(self, start=0, request_count=0):
        return self.ampache_query('genres', start, request_count)

    def ampache_query_tags(self, start=0, request_count=0):
        return self.ampache_query('tags', start, request_count)

    def ampache_query_videos(self, start=0, request_count=0):
        return self.ampache_query('videos', start, request_count)

    def upnp_init(self):
        if self.server:
            self.server.connection_manager_server.set_variable(
                0, 'SourceProtocolInfo',
                ['http-get:*:audio/mpeg:*',
                 'http-get:*:application/ogg:*',
                 'http-get:*:video/mp4:*',
                 'http-get:*:video/x-msvideo:*',
                 'http-get:*:video/avi:*',
                 'http-get:*:video/quicktime:*', ])

        self.containers[AUDIO_ALL_CONTAINER_ID] = \
            Container(AUDIO_ALL_CONTAINER_ID, ROOT_CONTAINER_ID, 'All tracks',
                      store=self,
                      children_callback=self.ampache_query_songs,
                      play_container=True)
        self.containers[AUDIO_ALL_CONTAINER_ID].childCount = self.songs
        self.containers[ROOT_CONTAINER_ID].add_child(
            self.containers[AUDIO_ALL_CONTAINER_ID])

        self.containers[AUDIO_ALBUM_CONTAINER_ID] = \
            Container(AUDIO_ALBUM_CONTAINER_ID, ROOT_CONTAINER_ID, 'Albums',
                      store=self,
                      children_callback=self.ampache_query_albums)
        self.containers[AUDIO_ALBUM_CONTAINER_ID].childCount = self.albums
        self.containers[ROOT_CONTAINER_ID].add_child(
            self.containers[AUDIO_ALBUM_CONTAINER_ID])

        self.containers[AUDIO_ARTIST_CONTAINER_ID] = \
            Container(AUDIO_ARTIST_CONTAINER_ID, ROOT_CONTAINER_ID, 'Artists',
                      store=self,
                      children_callback=self.ampache_query_artists)
        self.containers[AUDIO_ARTIST_CONTAINER_ID].childCount = self.artists
        self.containers[ROOT_CONTAINER_ID].add_child(
            self.containers[AUDIO_ARTIST_CONTAINER_ID])

        self.containers[AUDIO_PLAYLIST_CONTAINER_ID] = \
            Container(AUDIO_PLAYLIST_CONTAINER_ID, ROOT_CONTAINER_ID,
                      'Playlists',
                      store=self,
                      children_callback=self.ampache_query_playlists,
                      container_class=DIDLLite.PlaylistContainer)
        self.containers[AUDIO_PLAYLIST_CONTAINER_ID].childCount = \
            self.playlists
        self.containers[ROOT_CONTAINER_ID].add_child(
            self.containers[AUDIO_PLAYLIST_CONTAINER_ID])

        self.containers[AUDIO_GENRE_CONTAINER_ID] = \
            Container(AUDIO_GENRE_CONTAINER_ID, ROOT_CONTAINER_ID, 'Genres',
                      store=self,
                      children_callback=self.ampache_query_genres)
        self.containers[AUDIO_GENRE_CONTAINER_ID].childCount = self.genres
        self.containers[ROOT_CONTAINER_ID].add_child(
            self.containers[AUDIO_GENRE_CONTAINER_ID])

        self.containers[AUDIO_TAG_CONTAINER_ID] = \
            Container(AUDIO_TAG_CONTAINER_ID, ROOT_CONTAINER_ID, 'Tags',
                      store=self,
                      children_callback=self.ampache_query_tags)
        self.containers[AUDIO_TAG_CONTAINER_ID].childCount = self.tags
        self.containers[ROOT_CONTAINER_ID].add_child(
            self.containers[AUDIO_TAG_CONTAINER_ID])

        self.containers[VIDEO_CONTAINER_ID] = \
            Container(VIDEO_CONTAINER_ID, ROOT_CONTAINER_ID, 'Videos',
                      store=self,
                      children_callback=self.ampache_query_videos)
        self.containers[VIDEO_CONTAINER_ID].childCount = self.videos
        self.containers[ROOT_CONTAINER_ID].add_child(
            self.containers[VIDEO_CONTAINER_ID])

    def upnp_XBrowse(self, *args, **kwargs):
        try:
            ObjectID = kwargs['ObjectID']
        except Exception as e:
            self.debug(
                f'hmm, a Browse action and no ObjectID argument? '
                f'An XBox maybe?  [ERROR: {e}]')
            try:
                ObjectID = kwargs['ContainerID']
            except Exception:
                ObjectID = 0
        BrowseFlag = kwargs['BrowseFlag']
        Filter = kwargs['Filter']
        StartingIndex = int(kwargs['StartingIndex'])
        RequestedCount = int(kwargs['RequestedCount'])
        SortCriteria = kwargs['SortCriteria']
        parent_container = None
        requested_id = None

        if BrowseFlag == 'BrowseDirectChildren':
            parent_container = str(ObjectID)
        else:
            requested_id = str(ObjectID)

        self.info(f'upnp_Browse request {ObjectID} {BrowseFlag} '
                  f'{StartingIndex} {RequestedCount}')

        didl = DIDLLite.DIDLElement(
            upnp_client=kwargs.get('X_UPnPClient', ''),
            requested_id=requested_id,
            parent_container=parent_container)

        def build_response(tm):
            num_ret = didl.numItems()
            # if int(kwargs['RequestedCount']) != 0 and \
            #         num_ret != int(kwargs['RequestedCount']):
            #    num_ret = 0
            # if RequestedCount == 0 and tm-StartingIndex != num_ret:
            #    num_ret = 0
            r = {'Result': didl.toString(), 'TotalMatches': tm,
                 'NumberReturned': num_ret}

            self.info(f'upnp_Browse response {num_ret} {tm}')

            if hasattr(item, 'update_id'):
                r['UpdateID'] = item.update_id
            elif hasattr(self, 'update_id'):
                r['UpdateID'] = self.update_id  # FIXME
            else:
                r['UpdateID'] = 0

            return r

        def got_error(r):
            return r

        def process_result(result, found_item):
            if result is None:
                result = []
            if BrowseFlag == 'BrowseDirectChildren':
                lc = []

                def process_items(result, tm):
                    if result is None:
                        result = []
                    for i in result:
                        if i[0]:
                            didl.addItem(i[1])

                    return build_response(tm)

                for it in result:
                    md = defer.maybeDeferred(it.get_item)
                    lc.append(md)

                def got_child_count(count):
                    dl = defer.DeferredList(lc)
                    dl.addCallback(process_items, count)
                    return dl

                md = defer.maybeDeferred(found_item.get_child_count)
                md.addCallback(got_child_count)

                return md

            else:
                didl.addItem(result)
                total = 1

            return build_response(total)

        total = 0
        items = []

        wmc_mapping = getattr(self, 'wmc_mapping', None)
        if (kwargs.get('X_UPnPClient', '') == 'XBox' and
                wmc_mapping is not None and
                ObjectID in wmc_mapping):
            ''' fake a Windows Media Connect Server
            '''
            root_id = wmc_mapping[ObjectID]
            if callable(root_id):
                item = root_id()
                if item is not None:
                    if isinstance(item, list):
                        total = len(item)
                        if int(RequestedCount) == 0:
                            items = item[StartingIndex:]
                        else:
                            items = item[StartingIndex:
                                         StartingIndex + RequestedCount]
                    else:
                        d = defer.maybeDeferred(item.get_children,
                                                StartingIndex,
                                                StartingIndex + RequestedCount)
                        d.addCallback(process_result)
                        d.addErrback(got_error)
                        return d

            for i in items:
                didl.addItem(i.get_item())

            return build_response(total)

        root_id = ObjectID

        item = self.get_by_id(root_id)
        if item is None:
            return failure.Failure(errorCode(701))

        def proceed(result):
            if BrowseFlag == 'BrowseDirectChildren':
                md = defer.maybeDeferred(result.get_children, StartingIndex,
                                         StartingIndex + RequestedCount)
            else:
                md = defer.maybeDeferred(result.get_item)

            md.addCallback(process_result, result)
            md.addErrback(got_error)
            return md

        if isinstance(item, defer.Deferred):
            item.addCallback(proceed)
            return item
        else:
            return proceed(item)


if __name__ == '__main__':
    from coherence.base import Coherence

    def main():
        def got_result(result):
            print('got_result')

        def call_browse(ObjectID=0, StartingIndex=0, RequestedCount=0):
            r = f.backend.upnp_Browse(BrowseFlag='BrowseDirectChildren',
                                      RequestedCount=RequestedCount,
                                      StartingIndex=StartingIndex,
                                      ObjectID=ObjectID,
                                      SortCriteria='*',
                                      Filter='')
            r.addCallback(got_result)
            r.addErrback(got_result)

        def call_test(start, count):
            r = f.backend.ampache_query_artists(start, count)
            r.addCallback(got_result)
            r.addErrback(got_result)

        config = {}
        config['logmode'] = 'warning'
        c = Coherence(config)
        f = c.add_plugin('AmpacheStore',
                         url='http://localhost/ampache/server/xml.server.php',
                         key='password',
                         user=None)
        reactor.callLater(3, call_browse, 0, 0, 0)
        # reactor.callLater(3, call_browse, AUDIO_ALL_CONTAINER_ID, 0, 0)
        # reactor.callLater(3, call_browse, AUDIO_ARTIST_CONTAINER_ID, 0, 10)
        # reactor.callLater(3, call_browse, AUDIO_ALBUM_CONTAINER_ID, 0, 10)
        # reactor.callLater(3, call_test, 0, 10)

    reactor.callWhenRunning(main)
    reactor.run()
