coherence.backends.lastfm_storage — Cohen3 0.8.0 documentation

# Source code for coherence.backends.lastfm_storage

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

    # Copyright 2007, Frank Scholz <coherence@beebits.net>
    # Copyright 2007, Moritz Struebe <morty@gmx.net>

    # TODO: remove the below "terminal output" sample when this backend get fixed
    # INFO  lastFM_user                 Dez 14 17:35:27  Got new sessionid:
    #     '1488f34a1cbed7c9f4232f8fd563c3bd'
    #     (coherence/backends/lastfm_storage.py:60)
    # DEBUG lastFM_stream               Dez 14 17:35:53  render
    #     <GET /da525474-5357-4d1b-a894-76b1293224c9/1005 HTTP/1.1>
    #     (coherence/backends/lastfm_storage.py:148)
    # command GET
    # rest /user/e0362c757ef49169e9a0f0970cc2d367.mp3
    # headers {'icy-metadata': '1', 'host': 'kingpin5.last.fm',
    #          'te': 'trailers', 'connection': 'TE',
    #          'user-agent': 'gnome-vfs/2.12.0.19 neon/0.24.7'}
    # ProxyClient handleStatus HTTP/1.1 200 OK
    # ProxyClient handleHeader Content-Type audio/mpeg
    # ProxyClient handleHeader Content-Length 4050441
    # ProxyClient handleHeader Cache-Control no-cache, must-revalidate
    # DEBUG lastFM_stream               Dez 14 17:35:53  render
    #     <GET /da525474-5357-4d1b-a894-76b1293224c9/1005 HTTP/1.1>
    #     (coherence/backends/lastfm_storage.py:148)
    # command GET
    # rest /user/e0362c757ef49169e9a0f0970cc2d367.mp3
    # headers {'icy-metadata': '1', 'host': 'kingpin5.last.fm',
    #          'te': 'trailers', 'connection': 'TE',
    #          'user-agent': 'gnome-vfs/2.12.0.19 neon/0.24.7'}
    # ProxyClient handleStatus HTTP/1.1 403 Invalid ticket

    from hashlib import md5
    from urllib.parse import urlsplit

    from coherence import log
    from coherence.upnp.core import utils
    from coherence.backend import Backend
    from coherence.upnp.core.DIDLLite import classChooser, Container, Resource

    class LastFMUser(log.LogAble):
        logCategory = 'lastFM_user'

        user = None
        passwd = None
        host = 'ws.audioscrobbler.com'
        basepath = '/radio'
        sessionid = None
        parent = None
        getting_tracks = False
        tracks = []

        def __init__(self, user, passwd):
            log.LogAble.__init__(self)
            if user is None:
                self.warn('No User', )
            if passwd is None:
                self.warn('No Passwd', )
            self.user = user
            self.passwd = passwd

        def login(self):

            if self.sessionid is not None:
                self.warning('Session seems to be valid', )
                return

            def got_page(result):
                lines = result[0].split('\n')
                for line in lines:
                    tuple = line.rstrip().split('=', 1)
                    if len(tuple) == 2:
                        if tuple[0] == 'session':
                            self.sessionid = tuple[1]
                            self.info(f'Got new sessionid: {self.sessionid}')
                        if tuple[0] == 'base_url':
                            if self.host != tuple[1]:
                                self.host = tuple[1]
                                self.info(f'Got new host: {self.host}')
                        if tuple[0] == 'base_path':
                            if self.basepath != tuple[1]:
                                self.basepath = tuple[1]
                                self.info(f'Got new path: {self.basepath}')
                self.get_tracks()

            def got_error(error):
                self.warning(f'Login to LastFM Failed! {error}')
                self.debug(f'{error.getTraceback()}')

            # This function might be GPL!
            # Found this code in some other Projects, too.
            def hexify(s):
                result = ''
                for c in s:
                    result = result + ('%02x' % ord(c))
                return result

            password = hexify(md5(self.passwd).digest())
            req = \
                self.basepath + '/handshake.php/?version=1&platform=win&username='\
                + self.user + '&passwordmd5=' + password\
                + '&language=en&player=coherence'
            utils.getPage('http://' + self.host + req).addCallbacks(
                got_page, got_error, None, None, None, None)

        def get_tracks(self):
            if self.getting_tracks:
                return

            def got_page(result):
                result = utils.parse_xml(result, encoding='utf-8')
                self.getting_tracks = False
                print(self.getting_tracks)
                print('got Tracks')
                for track in result.findall('trackList/track'):
                    data = {}

                    def get_data(name):
                        # print track.find(name).text.encode('utf-8')
                        return track.find(name).text.encode('utf-8')

                    # Fixme: This section needs some work
                    print('adding Track')
                    data['mimetype'] = 'audio/mpeg'
                    data['name'] = get_data('creator') + ' - ' + get_data('title')
                    data['title'] = get_data('title')
                    data['artist'] = get_data('creator')
                    data['creator'] = get_data('creator')
                    data['album'] = get_data('album')
                    data['duration'] = get_data('duration')
                    # FIXME: Image is the wrong tag.
                    data['image'] = get_data('image')
                    data['url'] = track.find('location').text.encode('utf-8')
                    item = self.parent.store.append(data, self.parent)
                    self.tracks.append(item)

            def got_error(error):
                self.warning(f'Problem getting Tracks! {error}')
                self.debug(f'{error.getTraceback()}')
                self.getting_tracks = False

            self.getting_tracks = True
            req = \
                self.basepath + '/xspf.php?sk=' + self.sessionid \
                + '&discovery=0&desktop=1.3.1.1'
            utils.getPage('http://' + self.host + req).addCallbacks(
                got_page, got_error, None, None, None, None)

        def update(self, item):
            if 0 < self.tracks.count(item):
                while True:
                    track = self.tracks[0]
                    if track == item:
                        break
                    self.tracks.remove(track)
                    # Do not remove so the tracks to answer the browse
                    # request correctly.
                    # track.store.remove(track)
                    # del track

            # if len(self.tracks) < 5:
            self.get_tracks()

    class LFMProxyStream(utils.ReverseProxyResource, log.LogAble):
        logCategory = 'lastFM_stream'

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

            if path == '':
                path = '/'

            # print(f'ProxyStream init {host} {port} {path}')
            utils.ReverseProxyResource.__init__(self, host, port, path)

        def render(self, request):
            self.debug(f'render {request}')
            self.parent.store.LFM.update(self.parent)
            self.parent.played = True
            return utils.ReverseProxyResource.render(self, request)

    class LastFMItem(log.LogAble):
        logCategory = 'LastFM_item'

        def __init__(self, id, obj, parent, mimetype, urlbase, UPnPClass,
                     update=False):
            log.LogAble.__init__(self)
            self.id = id

            self.name = obj.get('name')
            self.title = obj.get('title')
            self.artist = obj.get('artist')
            self.creator = obj.get('creator')
            self.album = obj.get('album')
            self.duration = obj.get('duration')
            self.mimetype = mimetype

            self.parent = parent
            if parent:
                parent.add_child(self, update=update)

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

            self.item = UPnPClass(id, parent_id, self.title, False, self.creator)
            if isinstance(self.item, Container):
                self.item.childCount = 0
            self.child_count = 0
            self.children = []

            if len(urlbase) and urlbase[-1] != '/':
                urlbase += '/'

            if self.mimetype == 'directory':
                self.url = urlbase + str(self.id)
            else:
                self.url = urlbase + str(self.id)
                self.location = LFMProxyStream(obj.get('url'), self)
                # self.url = obj.get('url')

            if self.mimetype == 'directory':
                self.update_id = 0
            else:
                protocols = ('DLNA.ORG_PN=MP3',
                             'DLNA.ORG_CI=0',
                             'DLNA.ORG_OP=01',
                             'DLNA.ORG_FLAGS=01700000000000000000000000000000')
                res = Resource(
                    self.url,
                    f'http-get:*:{obj.get("mimetype")}:{";".join(protocols)}')
                res.size = -1  # None
                self.item.res.append(res)

        def remove(self):
            if self.parent:
                self.parent.remove_child(self)
            del self.item

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

        def remove_child(self, child):
            self.info(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

        def get_children(self, start=0, request_count=0):
            if request_count == 0:
                return self.children[start:]
            else:
                return self.children[start:request_count]

        def get_child_count(self):
            if self.mimetype == 'directory':
                return 100  # Some Testing, with strange Numbers: 0/lots
            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):
            return self.url

        def get_name(self):
            return self.name

        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) + ' @ ' + self.url + ' ' + self.name

    class LastFMStore(Backend):
        '''
        This is a backend to the LastFM.

        .. versionchanged:: 0.9.0

            * Migrated from louie/dispatcher to EventDispatcher
            * Introduced :class:`~coherence.backend.Backend`'s inheritance
        '''
        logCategory = 'lastFM_store'

        implements = ['MediaServer']

        def __init__(self, server, **kwargs):
            Backend.__init__(self, server, **kwargs)

            self.next_id = 1000
            self.config = kwargs
            self.name = kwargs.get('name', 'LastFMStore')

            self.update_id = 0
            self.store = {}
            self.server = server
            self.urlbase = 'http://ws.audioscrobbler.com/2.0/'

            self.wmc_mapping = {'4': 1000}

            self.init_completed = True

        def __repr__(self):
            return str(self.__class__).split('.')[-1]

        def append(self, obj, parent):
            if isinstance(obj, str):
                mimetype = 'directory'
            else:
                mimetype = obj['mimetype']

            UPnPClass = classChooser(mimetype)
            id = self.getnextID()
            update = False
            if hasattr(self, 'update_id'):
                update = True

            self.store[id] = LastFMItem(id, obj, parent, mimetype, self.urlbase,
                                        UPnPClass, update=update)
            self.store[id].store = self

            if hasattr(self, 'update_id'):
                self.update_id += 1
                if self.server:
                    self.server.content_directory_server.set_variable(
                        0, 'SystemUpdateID', self.update_id)
                if parent:
                    # value = '%d,%d' % (parent.get_id(),parent_get_update_id())
                    value = (parent.get_id(), parent.get_update_id())
                    if self.server:
                        self.server.content_directory_server.set_variable(
                            0, 'ContainerUpdateIDs', value)

            return self.store[id]

        def remove(self, item):
            try:
                parent = item.get_parent()
                item.remove()
                del self.store[int(id)]
                if hasattr(self, 'update_id'):
                    self.update_id += 1
                    if self.server:
                        self.server.content_directory_server.set_variable(
                            0, 'SystemUpdateID', self.update_id)
                    # value = '%d,%d' % (parent.get_id(),parent_get_update_id())
                    value = (parent.get_id(), parent.get_update_id())
                    if self.server:
                        self.server.content_directory_server.set_variable(
                            0, 'ContainerUpdateIDs', value)
            except (ValueError, KeyError):
                pass

        def len(self):
            return len(self.store)

        def get_by_id(self, id):
            if isinstance(id, str):
                id = id.split('@', 1)[0]
            elif isinstance(id, bytes):
                id = id.decode('utf-8').split('@', 1)[0]
            id = int(id)
            if id == 0:
                id = 1000
            try:
                return self.store[id]
            except KeyError:
                return None

        def getnextID(self):
            ret = self.next_id
            self.next_id += 1
            return ret

        def upnp_init(self):
            self.current_connection_id = None

            parent = self.append({'name': 'LastFM', 'mimetype': 'directory'}, None)

            self.LFM = LastFMUser(self.config.get('login'),
                                  self.config.get('password'))
            self.LFM.parent = parent
            self.LFM.login()

            if self.server:
                self.server.connection_manager_server.set_variable(
                    0, 'SourceProtocolInfo', ['http-get:*:audio/mpeg:*'],
                    default=True)

    def main():
        f = LastFMStore(None)

        def got_upnp_result(result):
            print(f'upnp {result}')

        f.upnp_init()

    if __name__ == '__main__':
        from twisted.internet import reactor

        reactor.callWhenRunning(main)
        reactor.run()