diff options
author | ComradeCrow <comradecrow@vivaldi.net> | 2025-01-26 22:26:00 -0800 |
---|---|---|
committer | ComradeCrow <comradecrow@vivaldi.net> | 2025-01-26 22:26:00 -0800 |
commit | 396448bf1c7f372474eca374cfecdb64fca854f1 (patch) | |
tree | 809681c52fcd4c28637bda6cbfec33d91ebca28e | |
parent | 750eafc90384e51d55fbea053e943f5fcffccd1f (diff) | |
download | podweb-396448bf1c7f372474eca374cfecdb64fca854f1.tar.gz |
Bugfixes and implement database
Start implementing database to track downloaded episode files added more cli commands retain comments in config and serverlist
-rw-r--r-- | .gitignore | 3 | ||||
-rwxr-xr-x | PodWeb.py | 315 | ||||
-rw-r--r-- | readme.md | 5 | ||||
-rw-r--r-- | requirements.txt | 1 |
4 files changed, 241 insertions, 83 deletions
diff --git a/.gitignore b/.gitignore index 55eb0a1..95ca0ac 100644 --- a/.gitignore +++ b/.gitignore @@ -158,4 +158,5 @@ cython_debug/ debug_* *.opml *.xml -*.json \ No newline at end of file +*.json +podcasts diff --git a/PodWeb.py b/PodWeb.py index b0f1f7c..cb407de 100755 --- a/PodWeb.py +++ b/PodWeb.py @@ -7,27 +7,39 @@ import xml.etree.ElementTree as xmlet import podcastparser import urllib import urllib.request +import urllib.parse import pprint +import logging +import sqlite3 + from ruamel.yaml import YAML global options -options = {"DEBUG": False, - "serverlist": os.path.normpath(os.path.join(os.path.expanduser("~/.config/podweb"), "serverlist")), - "podcastpath": ""} +options = { + "DEBUG": False, + "serverlist": os.path.normpath( + os.path.join(os.path.expanduser("~/.config/podweb"), "serverlist") + ), + "downloadlocation": os.path.expanduser("~/Podcasts"), +} yaml = YAML() +yaml.allow_duplicate_keys = True + -class PodWeb(): - - def __init__(self, - debug : bool = False, - config : None | str = None, - server_list : None | str = None, - download_location : None | str = None) -> None: +class PodWeb: + def __init__( + self, + debug: bool = False, + simulate: 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. + 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: @@ -38,157 +50,300 @@ class PodWeb(): ## site: https://example.com ## - name: example podcast 2 ## url: example.com/feed2.xml -''' + +""" + + if options["DEBUG"]: + log_level = logging.DEBUG + else: + log_level = logging.ERROR + self.log = logging.getLogger("PodWeb") + self.log.setLevel(log_level) + if not self.log.handlers: + ch = logging.StreamHandler() + ch.setLevel(log_level) + formatter = logging.Formatter("%(levelname)s:%(name)s:%(message)s") + ch.setFormatter(formatter) + self.log.addHandler(ch) 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") + self.db_path = self.config_path + self.db_filepath = "debug_podweb.db" + self.options["serverlist"] = os.path.join( + self.config_path, "debug_serverlist" + ) + self.options["downloadlocation"] = os.path.join( + self.config_path, "podcasts" + ) else: self.config_path = os.path.normpath(os.path.expanduser("~/.config/podweb")) self.config_filepath = os.path.join(self.config_path, "config.yaml") + self.db_path = os.path.expanduser("~/.local/share/podweb") + self.db_filepath = os.path.join(self.db_path, "podweb.db") if config: self.config_filepath = os.path.normpath(os.path.expanduser(config)) + self._open_db() self._load_config() + + self._update_config(self.options) if server_list: - self.options["serverlist"] = os.path.normpath(os.path.expanduser(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.options["downloadlocation"] = os.path.normpath( + os.path.expanduser(download_location) + ) + if not os.path.exists(self.options["downloadlocation"]): + os.makedirs(options["downloadlocation"]) self._load_serverlist() - def _load_config(self) -> None: + def __del__(self): + self._close_db() - if not os.path.exists(self.config_path): os.makedirs(self.config_path) + def _open_db(self) -> None: + """Opens SQLite database to track podcast episodes.""" + if not os.path.exists(self.db_path): + os.makedirs(self.db_path) + self.con = sqlite3.connect(self.db_filepath) + self.data = self.con.cursor() + self._create_tables() + + def _close_db(self) -> None: + self.con.close() + + def _create_tables(self) -> None: + self.data.execute("""CREATE TABLE IF NOT EXISTS "episodes" ( + "guid" TEXT NOT NULL UNIQUE, + "title" TEXT, + "description" TEXT, + "img" TEXT, + PRIMARY KEY("guid") + )""") + self.data.execute("""CREATE TABLE IF NOT EXISTS "downloads" ( + "guid" TEXT NOT NULL UNIQUE, + "filepath" TEXT NOT NULL UNIQUE, + PRIMARY KEY("guid"), + FOREIGN KEY("guid") REFERENCES "episodes"("guid") + )""") + + def _load_config(self) -> None: + """Loads current config""" + 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: + 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.load(f)) + with open(self.config_filepath, "r+t") as f: + data = yaml.load(f) + if data is None: + yaml.dump(self.options, f) + else: + self.options.update(data) - def _update_config(self, changed_option : dict) -> None: - '''Makes a change to the config file''' - with open(self.options["serverlist"], "w+") as f: + def _update_config(self, changed_option: dict) -> None: + """Makes a change to the config file""" + with open(self.config_filepath, "rt") as f: config_options = yaml.load(f) config_options.update(changed_option) - f.write(config_options) + with open(self.config_filepath, "wt") as f: + yaml.dump(config_options, f) - def _load_serverlist(self) -> list: - '''Loads the contents of the serverlist''' + def _load_serverlist(self, + do_return : bool = False): + """Loads the contents of the serverlist""" self._create_serverlist() - with open(self.options["serverlist"], "r+") as f: + with open(self.options["serverlist"], "r") as f: content = yaml.load(f) + if do_return: + return content if content: self.servers = content def _create_serverlist(self) -> None: - '''Checks if the serverlist does not exist and creates it if not''' + """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: + 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) + """Overwrites the current serverlist with the stored serverlist""" + serverlist = self._load_serverlist(True) if len(self.servers): - with open(self.options["serverlist"], "a") as f: + with open(self.options["serverlist"], "w") as f: + if serverlist is None: + f.write(self.DEFAULT_SERVERLIST_HEADING) yaml.dump(self.servers, f) - - def add_podcast(self, feedurl : str, name = None, category = None, site = None, img = None) -> None: + + 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: + if name is None or img is None or site is None: 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}) + if name is None: + name = parsed.get("title") + if img is None: + img = parsed.get("cover_url") + if site is None: + 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: + def import_opml(self, opml_path: str) -> None: body = xmlet.parse(source=opml_path).getroot().find("body") + if body is None: + raise SyntaxError("OPML does not have body tag") 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: + 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: + + 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): +@click.option("-d", "--debug", is_flag=True) +@click.option("--simulate", 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, + simulate: 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.obj = PodWeb( + debug=debug, + simulate=simulate, + 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.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': + 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"]}") + if i.get("name"): + name = f"{i['name']} - " + click.echo(f"{name}{i['url']}") + @cli.command() @click.argument("url") +@click.option( + "-F", + "--format", + type=click.Choice(["pprint", "json"], case_sensitive=False), + default="pprint", +) @click.pass_obj -def parse(obj, url): - click.echo(pprint.pformat(obj._parse_rss(url))) +def parse(obj, url, format): + if format == "pprint": + click.echo(pprint.pformat(obj._parse_rss(url))) + else: + click.echo(json.dumps(obj._parse_rss(url), indent=4, separators=(",", ": "))) + @cli.command() @click.argument("filepath", type=click.Path(exists=True)) -@click.option("-F", "--format", - type=click.Choice(['pprint', 'json'], - case_sensitive=False), - default = "pprint") +@click.option( + "-F", + "--format", + type=click.Choice(["pprint", "json"], case_sensitive=False), + default="pprint", +) @click.pass_obj def parse_file(obj, filepath, format): if format == "pprint": click.echo(pprint.pformat(obj._parse_local_rss(filepath))) - if format == "json": - click.echo(json.dumps(obj._parse_local_rss(filepath), indent=4, separators=(',', ': '))) + else: + click.echo( + json.dumps(obj._parse_local_rss(filepath), indent=4, separators=(",", ": ")) + ) + + +@cli.command() +@click.argument("url") +@click.option("-n", "--name") +@click.option("-c", "--category") +@click.option("-s", "--site") +@click.option("-i", "--img") +@click.pass_obj +def add_podcast( + obj, + url: str, + name: str | None, + category: str | None, + site: str | None, + img: str | None, +): + obj.add_podcast(feedurl=url, name=name, category=category, site=site, img=img) + +@cli.command() +@click.argument("opml-file", type=click.Path(exists=True)) +@click.pass_obj +def import_opml(obj, opml_file : str): + obj.import_opml(opml_file) + click.echo(F"imported {opml_file}") if __name__ == "__main__": cli() diff --git a/readme.md b/readme.md index e423f9a..cce4573 100644 --- a/readme.md +++ b/readme.md @@ -1,2 +1,3 @@ -#PodWeb -A small in-progress cli podcast fetcher \ No newline at end of file +# PodWeb + +A small in-progress cli podcast fetcher diff --git a/requirements.txt b/requirements.txt index 08af916..9142357 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pytest >= 4.6 pytest-cov coverage ruamel.yaml + |