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


[docs]class RedirectingReverseProxyUriResource(ReverseProxyUriResource):
[docs] def render(self, request): self.uri = self.follow_redirect(self.uri) self.resetUri(self.uri) return ReverseProxyUriResource.render(self, request)
[docs] 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
[docs]class FeedStorageConfigurationException(Exception): pass
[docs]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 = []
[docs] 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:]
[docs] def get_child_count(self): '''returns the number of children in this container''' return len(self.children)
[docs]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)
[docs]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()
[docs] 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
[docs] 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')