Refactoring work to make this a real program

master
bspeice 2016-05-17 10:40:23 -04:00
parent 5853c86a2d
commit fcc280b96c
13 changed files with 215 additions and 174 deletions

View File

@ -13,23 +13,7 @@
<ConfirmationsSetting value="0" id="Add" /> <ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" /> <ConfirmationsSetting value="0" id="Remove" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="Python 3.5.1 (C:\Users\Bradlee Speice\Anaconda3\python.exe)" project-jdk-type="Python SDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" default="false" assert-keyword="true" jdk-15="true" project-jdk-name="Python 3.5.1 (C:\Users\Bradlee Speice\Anaconda3\python.exe)" project-jdk-type="Python SDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
<component name="masterDetails">
<states>
<state key="ProjectJDKs.UI">
<settings>
<last-edited>Python 3.5.1 (C:\Users\Bradlee Speice\Anaconda3\python.exe)</last-edited>
<splitter-proportions>
<option name="proportions">
<list>
<option value="0.2" />
</list>
</option>
</splitter-proportions>
</settings>
</state>
</states>
</component>
</project> </project>

View File

@ -1,12 +1,15 @@
# Format: (all args are passed to __init__ as kwargs # Format (all args are passed to __init__ as kwargs:
# #
# <mountpoint>: # <mountpoint>:
# class: <feed_class> # class: <feed_class>
# args: # args:
# key: value # key: value
subfactory-show: server:
package: bassdrive port: 10000
class: BassdriveFeed
args: podcasts:
url: http://archives.bassdrivearchive.com/1%20-%20Monday/Subfactory%20Show%20-%20DJ%20Spim/ subfactory-show:
logo: http://www.bassdrive.com/img/radio_schedule_entries/image/original/subfactory-web-add-56.jpg class: podcasters.BassdriveFeed
args:
url: http://archives.bassdrivearchive.com/1%20-%20Monday/Subfactory%20Show%20-%20DJ%20Spim/
logo: http://www.bassdrive.com/img/radio_schedule_entries/image/original/subfactory-web-add-56.jpg

View File

@ -2,7 +2,9 @@
<module type="PYTHON_MODULE" version="4"> <module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>

View File

@ -1,30 +0,0 @@
"""
Given a configuration file, set up everything needed to kick
off the server.
"""
from importlib import import_module
import yaml
from pyramid.config import Configurator
from os.path import expanduser, join
# Needed for import_module call
# noinspection PyUnresolvedReferences
import modules
def build_configuration(conf=None) -> Configurator:
if conf is None:
conf = join(expanduser('~'), '.repodrc')
with open(conf) as conf_file:
conf_dict = yaml.load(conf_file)
server_conf = Configurator()
for mountpoint, feed in conf_dict.items():
feed_package = import_module('modules.' + feed['package'])
feed_class = getattr(feed_package, feed['class'])
feed_instance = feed_class(**feed['args'])
server_conf.add_route(mountpoint, '/' + mountpoint + '/')
server_conf.add_view(feed_instance.view, route_name=mountpoint)
return server_conf

View File

@ -1,10 +0,0 @@
from wsgiref.simple_server import make_server
from conf_parser import build_configuration
def start_server():
app = build_configuration().make_wsgi_app()
server = make_server('0.0.0.0', 8080, app)
server.serve_forever()
if __name__ == '__main__':
start_server()

38
src/conf_parser.py Normal file
View File

@ -0,0 +1,38 @@
"""
Given a configuration file, set up everything needed to kick
off the server.
"""
from importlib import import_module
import yaml
from pyramid.config import Configurator
def build_configurator(podcasts: dict) -> Configurator:
server_conf = Configurator()
for mountpoint, feed in podcasts:
package, class_name = feed['class'].rsplit('.', 1)
feed_package = import_module(package)
feed_class = getattr(feed_package, class_name)
feed_instance = feed_class(**feed['args'])
server_conf.add_route(mountpoint, '/' + mountpoint + '/')
server_conf.add_view(feed_instance.view, route_name=mountpoint)
def build_configuration_text(file_str: str) -> (dict, Configurator):
conf_dict = yaml.load(file_str)
server_opts = conf_dict.get('server', None)
podcasts = build_configurator(conf_dict['podcasts'])
return server_opts, podcasts
def build_configuration(file_name) -> (dict, Configurator):
try:
with open(file_name) as conf_file:
return build_configuration_text(conf_file.read())
except FileNotFoundError:
print("Could not locate configuration file " +
"(does {} exist?)".format(file_name))
raise

View File

@ -1,18 +1,19 @@
""" """
Base skeleton for what needs to be implemented by a podcast provider Base skeleton for what needs to be implemented by a podcast provider
""" """
from feedgen.feed import FeedGenerator from feedgen.feed import FeedGenerator
from pyramid.response import Response from pyramid.response import Response
class BasePodcast(): class BasePodcast():
def build_feed(self) -> FeedGenerator: def build_feed(self) -> FeedGenerator:
"Return a list of all episodes, in descending date order" "Return a list of all episodes, in descending date order"
pass pass
def view(self, request): # noinspection PyUnusedLocal
fg = self.build_feed() def view(self, request):
response = Response(fg.rss_str(pretty=True)) fg = self.build_feed()
response.content_type = 'application/rss+xml' response = Response(fg.rss_str(pretty=True))
return response response.content_type = 'application/rss+xml'
return response

View File

@ -1,91 +1,94 @@
""" """
Podcast provider for the Bassdrive Archives Podcast provider for the Bassdrive Archives
""" """
from html.parser import HTMLParser from datetime import datetime
from urllib.parse import unquote from html.parser import HTMLParser
from urllib.parse import unquote
import requests
from feedgen.feed import FeedGenerator import requests
from feedgen.feed import FeedGenerator
from podcast import BasePodcast from pytz import UTC
from datetime import datetime
from pytz import UTC from podcasters.base import BasePodcast
class BassdriveParser(HTMLParser): class BassdriveParser(HTMLParser):
record_link_text = False def error(self, message):
link_url = '' return super().error(message)
def __init__(self, *args, **kwargs): record_link_text = False
super().__init__(*args, **kwargs) link_url = ''
self.links = []
def __init__(self, *args, **kwargs):
def handle_starttag(self, tag, attrs): # noinspection PyArgumentList
href = '' super().__init__(*args, **kwargs)
for attr, val in attrs: self.links = []
if attr == 'href':
href = val def handle_starttag(self, tag, attrs):
href = ''
if tag == 'a' and href.find('mp3') != -1: for attr, val in attrs:
self.record_link_text = True if attr == 'href':
self.link_url = href href = val
def handle_data(self, data): if tag == 'a' and href.find('mp3') != -1:
if self.record_link_text: self.record_link_text = True
self.links.append((data, self.link_url)) self.link_url = href
self.record_link_text = False
def handle_data(self, data):
def get_links(self): if self.record_link_text:
# Reverse to sort in descending date order self.links.append((data, self.link_url))
return self.links self.record_link_text = False
def clear_links(self): def get_links(self):
self.links = [] # Reverse to sort in descending date order
return self.links
class BassdriveFeed(BasePodcast): def clear_links(self):
def __init__(self, *args, **kwargs): self.links = []
self.url = kwargs['url']
self.logo = kwargs['logo']
# Get the title and DJ while handling trailing slash class BassdriveFeed(BasePodcast):
url_pretty = unquote(self.url) def __init__(self, *args, **kwargs):
elems = filter(lambda x: x, url_pretty.split('/')) self.url = kwargs['url']
self.title, self.dj = list(elems)[-1].split(' - ') self.logo = kwargs['logo']
# Get the title and DJ while handling trailing slash
def build_feed(self): url_pretty = unquote(self.url)
"Build the feed given our existing URL" elems = filter(lambda x: x, url_pretty.split('/'))
# Get all the episodes self.title, self.dj = list(elems)[-1].split(' - ')
page_content = str(requests.get(self.url).content)
parser = BassdriveParser() def build_feed(self):
parser.feed(page_content) "Build the feed given our existing URL"
links = parser.get_links() # Get all the episodes
page_content = str(requests.get(self.url).content)
# And turn them into something usable parser = BassdriveParser()
fg = FeedGenerator() parser.feed(page_content)
#fg.load_extension('podcast') links = parser.get_links()
fg.id(self.url)
fg.title(self.title) # And turn them into something usable
fg.description(self.title) fg = FeedGenerator()
fg.author({'name': self.dj}) fg.id(self.url)
fg.language('en') fg.title(self.title)
fg.link({'href': self.url, 'rel': 'alternate'}) fg.description(self.title)
fg.logo(self.logo) fg.author({'name': self.dj})
fg.language('en')
for link in links: fg.link({'href': self.url, 'rel': 'alternate'})
fe = fg.add_entry() fg.logo(self.logo)
fe.author({'name': self.dj})
fe.title(link[0]) for link in links:
fe.description(link[0]) fe = fg.add_entry()
fe.enclosure(self.url + link[1], 0, 'audio/mpeg') fe.author({'name': self.dj})
fe.title(link[0])
# Bassdrive always uses date strings of fe.description(link[0])
# [yyyy.mm.dd] with 0 padding, so that fe.enclosure(self.url + link[1], 0, 'audio/mpeg')
# makes our lives easy
date_start = link[0].find('[') # Bassdrive always uses date strings of
date_str = link[0][date_start:date_start+12] # [yyyy.mm.dd] with 0 padding on days and months,
published = datetime.strptime(date_str, '[%Y.%m.%d]') # so that makes our lives easy
fe.pubdate(UTC.localize(published)) date_start = link[0].find('[')
fe.guid((link[0])) date_str = link[0][date_start:date_start+12]
published = datetime.strptime(date_str, '[%Y.%m.%d]')
parser.clear_links() fe.pubdate(UTC.localize(published))
return fg fe.guid((link[0]))
parser.clear_links()
return fg

38
src/server.py Normal file
View File

@ -0,0 +1,38 @@
import argparse
from wsgiref.simple_server import make_server
from conf_parser import build_configuration
from os.path import expanduser, join
# noinspection PyUnresolvedReferences
def start_server(cmd_args: dict):
try:
server_conf, configurator = build_configuration(cmd_args.configuration)
app = configurator.make_wsgi_app()
server = make_server(cmd_args.host, cmd_args.port, app)
server.serve_forever()
except FileNotFoundError:
print("Unable to find configuration file. Does {} exist?"
.format(cmd_args.configuration))
except AttributeError:
print("Unable to parse configuration file. Is {} a valid YML file?"
.format(cmd_args.configuration))
except KeyError:
print('Unable to parse configuration file. Is there a `podcasts`'
'section?')
if __name__ == '__main__':
default_rc = join(expanduser('~'), '.repodrc')
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', help='Run server in verbose mode')
parser.add_argument('--port', type=int, default=10000,
help='Port to use when starting the server')
parser.add_argument('--host', type=str, default='0.0.0.0',
help='Host address to start the server')
parser.add_argument('--configuration', type=str, default=default_rc,
help='Configuration file to start the server')
args = parser.parse_args()
start_server(args)

0
src/tests/__init__.py Normal file
View File

View File

@ -0,0 +1,12 @@
from unittest import TestCase
import conf_parser
class TestBuild_configurator(TestCase):
def test_build_configurator(self):
try:
# noinspection PyTypeChecker
conf_parser.build_configurator(None)
self.fail("Must have dictionary to set up configurator")
except TypeError:
pass