coherence.backends.ampache_storage — Cohen3 0.8.0 documentation

# Source code for coherence.backends.ampache_storage

    # 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()