coherence.backends.feed_storage — Cohen3 0.8.0 documentation

# Source code for coherence.backends.feed_storage

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

    # Copyright 2009, Dominik Ruf <dominikruf at googlemail dot com>

    import http.client
    import urllib.error
    import urllib.parse
    import urllib.request
    from urllib.parse import urlsplit
    from xml.etree.ElementTree import ElementTree

    from coherence.backend import BackendItem
    from coherence.backend import BackendStore
    from coherence.upnp.core import DIDLLite
    from coherence.upnp.core.utils import ReverseProxyUriResource

    try:
        import feedparser
    except ImportError:
        raise ImportError('''
            This backend depends on the feedparser module.
            You can get it at http://www.feedparser.org/.''')

    MIME_TYPES_EXTENTION_MAPPING = {'mp3': 'audio/mpeg', }

    ROOT_CONTAINER_ID = 0
    AUDIO_ALL_CONTAINER_ID = 51
    AUDIO_ARTIST_CONTAINER_ID = 52
    AUDIO_ALBUM_CONTAINER_ID = 53
    VIDEO_FOLDER_CONTAINER_ID = 54

    class RedirectingReverseProxyUriResource(ReverseProxyUriResource):
        def render(self, request):
            self.uri = self.follow_redirect(self.uri)
            self.resetUri(self.uri)
            return ReverseProxyUriResource.render(self, request)

        def follow_redirect(self, uri):
            netloc, path, query, fragment = urlsplit(uri)[1:]
            conn = http.client.HTTPConnection(netloc)
            conn.request('HEAD', f'{path}?{query}#{fragment}')
            res = conn.getresponse()
            if res.status == 301 or res.status == 302:
                return self.follow_redirect(res.getheader('location'))
            else:
                return uri

    class FeedStorageConfigurationException(Exception):
        pass

    class FeedContainer(BackendItem):
        def __init__(self, parent_id, id, title):
            BackendItem.__init__(self)
            self.id = id
            self.parent_id = parent_id
            self.name = title
            self.mimetype = 'directory'
            self.item = DIDLLite.Container(self.id, self.parent_id, self.name)

            self.children = []

        def get_children(self, start=0, end=0):
            '''returns all the chidlren of this container'''
            if end != 0:
                return self.children[start:end]
            return self.children[start:]

        def get_child_count(self):
            '''returns the number of children in this container'''
            return len(self.children)

    class FeedEnclosure(BackendItem):
        def __init__(self, store, parent, id, title, enclosure):
            BackendItem.__init__(self)
            self.store = store
            self.parent = parent
            self.external_id = id
            self.name = title
            self.location = RedirectingReverseProxyUriResource(
                enclosure.url.encode('latin-1'))

            # doing this because some (Fraunhofer Podcast)
            # feeds say there mime type is audio/x-mpeg
            # which at least my XBOX doesn't like
            ext = enclosure.url.rsplit('.', 1)[0]
            if ext in MIME_TYPES_EXTENTION_MAPPING:
                mime_type = MIME_TYPES_EXTENTION_MAPPING[ext]
            else:
                mime_type = enclosure.type
            if enclosure.type.startswith('audio'):
                self.item = DIDLLite.AudioItem(id, parent, self.name)
            elif enclosure.type.startswith('video'):
                self.item = DIDLLite.VideoItem(id, parent, self.name)
            elif enclosure.type.startswith('image'):
                self.item = DIDLLite.ImageItem(id, parent, self.name)

            res = DIDLLite.Resource(f'{store.urlbase}{id:d}',
                                    f'http-get:*:{mime_type}:*')

            self.item.res.append(res)

    class FeedStore(BackendStore):
        '''a general feed store'''

        logCategory = 'feed_store'
        implements = ['MediaServer']

        def __init__(self, server, **kwargs):
            BackendStore.__init__(self, server, **kwargs)
            self.name = kwargs.get('name', 'Feed Store')
            self.urlbase = kwargs.get('urlbase', '')
            if (len(self.urlbase) > 0 and
                    self.urlbase[len(self.urlbase) - 1] != '/'):
                self.urlbase += '/'
            self.feed_urls = kwargs.get('feed_urls')
            self.opml_url = kwargs.get('opml_url')
            if not (self.feed_urls or self.opml_url):
                raise FeedStorageConfigurationException(
                    'either feed_urls or opml_url has to be set')
            if self.feed_urls and self.opml_url:
                raise FeedStorageConfigurationException(
                    'only feed_urls OR opml_url can be set')

            self.server = server
            self.refresh = \
                int(kwargs.get('refresh', 1)) * (60 * 60)  # TODO: not used yet
            self.store = {}
            self.wmc_mapping = {
                '4': str(AUDIO_ALL_CONTAINER_ID),  # all tracks
                '7': str(AUDIO_ALBUM_CONTAINER_ID),  # all albums
                '6': str(AUDIO_ARTIST_CONTAINER_ID),  # all artists
                '15': str(VIDEO_FOLDER_CONTAINER_ID),  # all videos
            }

            self.store[ROOT_CONTAINER_ID] = FeedContainer(
                -1, ROOT_CONTAINER_ID, self.name)
            self.store[AUDIO_ALL_CONTAINER_ID] = FeedContainer(
                -1, AUDIO_ALL_CONTAINER_ID, 'AUDIO_ALL_CONTAINER')
            self.store[AUDIO_ALBUM_CONTAINER_ID] = FeedContainer(
                -1, AUDIO_ALBUM_CONTAINER_ID, 'AUDIO_ALBUM_CONTAINER')
            self.store[VIDEO_FOLDER_CONTAINER_ID] = FeedContainer(
                -1, VIDEO_FOLDER_CONTAINER_ID, 'VIDEO_FOLDER_CONTAINER')

            try:
                self._update_data()
            except Exception as e:
                self.error(f'error while updateing the feed '
                           f'content for {self.name}: {str(e)}')
            self.init_completed()

        def get_by_id(self, id):
            '''returns the item according to the DIDLite id'''
            if isinstance(id, str):
                id = id.split('@', 1)[0]
            elif isinstance(id, bytes):
                id = id.decode('utf-8').split('@', 1)[0]
            try:
                return self.store[int(id)]
            except (ValueError, KeyError):
                self.info(
                    f'can\'t get item {int(id):d} from {self.name} feed storage')
            return None

        def _update_data(self):
            '''get the feed xml, parse it, etc.'''
            feed_urls = []
            if (self.opml_url):
                tree = ElementTree(file=urllib.request.urlopen(self.opml_url))
                body = tree.find('body')
                for outline in body.findall('outline'):
                    feed_urls.append(outline.attrib['url'])

            if (self.feed_urls):
                feed_urls = self.feed_urls.split()
            container_id = 100
            item_id = 1001
            for feed_url in feed_urls:
                netloc, path, query, fragment = urlsplit(feed_url)[1:]
                conn = http.client.HTTPConnection(netloc)
                conn.request('HEAD', f'{path}?{query}#{fragment}')
                res = conn.getresponse()
                if res.status >= 400:
                    self.warning(
                        f'error getting {feed_url} status code: {res.status:d}')
                    continue
                fp_dict = feedparser.parse(feed_url)
                name = fp_dict.feed.title
                self.store[container_id] = FeedContainer(ROOT_CONTAINER_ID,
                                                         container_id, name)
                self.store[ROOT_CONTAINER_ID].children.append(
                    self.store[container_id])
                self.store[VIDEO_FOLDER_CONTAINER_ID].children.append(
                    self.store[container_id])
                self.store[AUDIO_ALBUM_CONTAINER_ID].children.append(
                    self.store[container_id])
                for item in fp_dict.entries:
                    for enclosure in item.enclosures:
                        self.store[item_id] = FeedEnclosure(
                            self, container_id, item_id,
                            f'{item_id:04d} - {item.title}', enclosure)
                        self.store[container_id].children.append(
                            self.store[item_id])
                        if enclosure.type.startswith('audio'):
                            self.store[AUDIO_ALL_CONTAINER_ID].children.append(
                                self.store[item_id])
                            if not isinstance(self.store[container_id].item,
                                              DIDLLite.MusicAlbum):
                                self.store[container_id].item = \
                                    DIDLLite.MusicAlbum(
                                        container_id, AUDIO_ALBUM_CONTAINER_ID,
                                        name)

                        item_id += 1
                if container_id <= 1000:
                    container_id += 1
                else:
                    raise Exception('to many containers')