#!/usr/bin/env python3
import os
import click
import yaml
import xml.etree.ElementTree as xmlet
import podcastparser
import urllib
import urllib.request
import pprint
global options
options = {"DEBUG": False,
"serverlist": os.path.normpath(os.path.join(os.path.expanduser("~/.config/podweb"), "serverlist")),
"podcastpath": ""}
class PodWeb():
def __init__(self,
debug : bool = False,
config : None | str = None,
server_list : None | str = None,
download_location : None | str = None) -> None:
self.options = options
self.options.update({"DEBUG": debug})
self.servers = []
self.DEFAULT_SERVERLIST_HEADING = '''## You can add podcast xml feeds here.
## You can also optionally add categories, website url, image urls, and names for the podcasts.
## The order of category, name, and url does not matter.
## Here are some example entries:
## - category: example category
## name: example podcast 1
## url: https://example.com/feed.xml
## img: https://example.com/image.jpg
## site: https://example.com
## - name: example podcast 2
## url: example.com/feed2.xml
'''
if self.options["DEBUG"]:
self.config_path = os.path.abspath(os.path.curdir)
self.config_filepath = "debug_config.yaml"
self.options["serverlist"] = os.path.join(self.config_path, "debug_serverlist")
else:
self.config_path = os.path.normpath(os.path.expanduser("~/.config/podweb"))
self.config_filepath = os.path.join(self.config_path, "config.yaml")
if config:
self.config_filepath = os.path.normpath(os.path.expanduser(config))
if server_list:
self.options["serverlist"] = os.path.normpath(os.path.expanduser(server_list))
if download_location:
self.options["podcastpath"] = os.path.normpath(os.path.expanduser(download_location))
self._load_config()
self._load_serverlist()
def _load_config(self) -> None:
if not os.path.exists(self.config_path): os.makedirs(self.config_path)
if not os.path.isfile(self.config_filepath):
with open(self.config_filepath, "w+") as f:
yaml.dump(self.options, f)
else:
with open(self.config_filepath, "r+") as f:
self.options.update(yaml.full_load(f))
def _update_config(self, changed_option : dict) -> None:
'''Makes a change to the config file'''
with open(self.options["serverlist"], "w+") as f:
config_options = yaml.full_load(f)
config_options.update(changed_option)
f.write(config_options)
def _load_serverlist(self) -> list:
'''Loads the contents of the serverlist'''
self._create_serverlist()
with open(self.options["serverlist"], "r+") as f:
content = yaml.full_load(f)
if content:
self.servers = content
def _create_serverlist(self) -> None:
'''Checks if the serverlist does not exist and creates it if not'''
if not os.path.isfile(self.options["serverlist"]):
with open(self.options["serverlist"], "w+") as f:
f.write(self.DEFAULT_SERVERLIST_HEADING)
def _update_serverlist(self) -> None:
'''This is destructive and overwrites the current serverlist with the stored serverlist'''
with open(self.options["serverlist"], "w+") as f:
f.write(self.DEFAULT_SERVERLIST_HEADING)
if len(self.servers):
with open(self.options["serverlist"], "a") as f:
yaml.dump(self.servers, f)
def add_podcast(self, feedurl : str, name = None, category = None, site = None, img = None) -> None:
feedparse = urllib.parse.urlparse(feedurl)
for i in self.servers:
iparse = urllib.parse.urlparse(i["url"])
if iparse.hostname == feedparse.hostname and iparse.path == feedparse.path:
return None
new_feed = {"url": feedurl}
if not name or not img or not site:
parsed = podcastparser.parse(feedurl, urllib.request.urlopen(feedurl))
if not name: name = parsed.get("title")
if not img: img = parsed.get("cover_url")
if not site: site = parsed.get("link")
if name: new_feed.update({"name": name})
if site: new_feed.update({"site": site})
if img: new_feed.update({"img": img})
if category: new_feed.update({"category": category})
self.servers.append(new_feed)
self._update_serverlist()
def import_opml(self, opml_path : str) -> None:
body = xmlet.parse(source=opml_path).getroot().find("body")
for child in body:
i = child.attrib
if i["type"] == "rss":
self.add_podcast(feedurl = i["xmlUrl"],
name = i.get("text"),
site = i.get("htmlUrl"),
img = i.get("imageUrl"))
def _parse_rss(self, url : str) -> dict:
parsed = podcastparser.parse(url, urllib.request.urlopen(url))
return parsed
def _parse_local_rss(self, file : str) -> dict:
with open(file, "rb") as f:
parsed = podcastparser.parse(file, f)
return parsed
@click.group()
@click.pass_context
@click.option('-d', '--debug', is_flag=True)
@click.option('--config', default=None)
@click.option('--server-list', default=None)
@click.option('--download-location', default=None)
def cli(ctx,
debug : bool,
config : None | str,
server_list : None | str,
download_location : None | str):
"""a simple podfetcher for the CLI."""
ctx.obj = PodWeb(debug = debug,
config = config,
server_list = server_list,
download_location = download_location)
ctx.show_default = True
@cli.command()
@click.argument("setting",
type=click.Choice(['configlocation', 'serverlistlocation', 'downloadlocation', 'servers'],
case_sensitive=False))
@click.pass_obj
def get_setting(obj, setting):
if setting == "configlocation":
click.echo(obj.config_filepath)
if setting == 'serverlistlocation':
click.echo(obj.options["serverlist"])
if setting == "downloadlocation":
click.echo(obj.options["podcastpath"])
if setting == "servers":
for i in obj.servers:
name = ""
if i.get("name"): name = F"{i["name"]} - "
click.echo(F"{name}{i["url"]}")
@cli.command()
@click.argument("url")
@click.pass_obj
def parse(obj, url):
click.echo(pprint.pformat(obj._parse_rss(url)))
@cli.command()
@click.argument("filepath", type=click.Path(exists=True))
@click.pass_obj
def parse_file(obj, filepath):
click.echo(pprint.pformat(obj._parse_local_rss(filepath)))
if __name__ == "__main__":
cli()