diff options
author | Wolfgang Müller | 2024-09-19 20:26:24 +0200 |
---|---|---|
committer | Wolfgang Müller | 2024-09-19 20:26:24 +0200 |
commit | 06a4bcc74f8a9deaa7b28f5cca1982caf83b6db3 (patch) | |
tree | 6346461b4de6fcd8ab5ef9af9f1bd63f24c7d5aa | |
parent | ae449a426f047c3574749b6ae1f5d2f5c28ad640 (diff) | |
download | later-06a4bcc74f8a9deaa7b28f5cca1982caf83b6db3.tar.gz |
Move functionality into classes
This should disentangle the logic a bit and make the entire code base
easier to work with. The watch_later entries are now kept in a
WatchLaterEntry dataclass which supports pretty-printing with __repr__.
Fetching of titles is now done through a dedicated TitleMap class which
imports yt_dlp as late as possible, speeding up printing of entries that
don't need to be updated and avoiding the import entirely if everything
can be fetched from cache. Otherwise there should not be any functional
change.
-rwxr-xr-x | later | 151 |
1 files changed, 94 insertions, 57 deletions
@@ -6,6 +6,7 @@ import json import os import re import sys +from dataclasses import dataclass from datetime import datetime as dt @@ -35,6 +36,92 @@ later_cache_dir = os.path.join(xdg_cache_home, "later") title_map_file = os.path.join(later_cache_dir, "titles.json") +@dataclass +class WatchLaterEntry: + name: str + path: str + mtime: dt + + def __repr__(self): + def format_time(time): + now = dt.now() + if time < now - datetime.timedelta(days=7): + return time.strftime("%b %d %Y") + return time.strftime("%b %d %H:%M") + + output = [format_time(self.mtime), self.name] + + if title := title_map.get(self.name): + output.append(f"# {title}") + + return "\t".join(output) + + +class TitleMap: + def __init__(self, path, update=False): + self.map = {} + self.path = path + self.update = update + self.commit_to_disk = update + self.ytdl = None + + try: + with open(path, "r") as handle: + self.map = json.load(handle) + except FileNotFoundError: + pass + except json.decoder.JSONDecodeError: + # Clobber the title cache if it was corrupted + self.commit_to_disk = True + except Exception as err: + sys.exit(f"later: {err}") + + def get(self, key): + title = self.map.get(key) + + if title or title == "": + return title + + if not re.fullmatch(r"https?://.*", key): + return None + + if self.update: + return self.extract(key) + + def extract(self, key): + # Make painstakingly sure that we only do this when absolutely + # necessary: importing yt_dlp is noticeably slow :( + import yt_dlp + + if not self.ytdl: + self.ytdl = yt_dlp.YoutubeDL({"logger": YTDLPLogger()}) + + try: + info = self.ytdl.extract_info(key, download=False) + + # The generic extractor uses the filename part of the url as the + # title. Since we already display the URL, this adds no extra + # information. + if info["extractor"] == "generic": + self.map[key] = "" + else: + self.map[key] = info["title"] + + return self.map.get(key) + + except yt_dlp.utils.DownloadError: + pass + + def maybe_commit(self): + if not self.commit_to_disk: + return + + os.makedirs(later_cache_dir, exist_ok=True) + + with open(self.path, "w") as handle: + json.dump(self.map, handle) + + def entries(): def get_mtime(entry): return entry.stat().st_mtime @@ -56,7 +143,7 @@ def entries(): if name == "redirect entry": continue - yield entry.path, name, mtime + yield WatchLaterEntry(name=name, path=entry.path, mtime=mtime) parser = argparse.ArgumentParser( @@ -70,59 +157,9 @@ parser.add_argument( ) args = parser.parse_args() -if args.update_titles: - import yt_dlp - - yt_dlp_opts = { - "logger": YTDLPLogger(), - } - ytdl = yt_dlp.YoutubeDL(yt_dlp_opts) - -write_title_map = args.update_titles - -try: - with open(title_map_file, "r") as handle: - title_map = json.load(handle) -except FileNotFoundError: - title_map = {} -except json.decoder.JSONDecodeError: - title_map = {} - write_title_map = True -except Exception as err: - sys.exit(f"later: {err}") - -for path, name, mtime in entries(): - if args.update_titles and name not in title_map: - if re.fullmatch(r"https?://.*", name): - try: - info = ytdl.extract_info(name, download=False) - - # The generic extractor uses the filename part of the url as the - # title. Since we already display the URL, this adds no extra - # information. - if info["extractor"] == "generic": - title_map[name] = "" - else: - title_map[name] = info["title"] - - except yt_dlp.utils.DownloadError: - pass - - def format_time(time): - now = dt.now() - if time < now - datetime.timedelta(days=7): - return time.strftime("%b %d %Y") - return time.strftime("%b %d %H:%M") - - output = [format_time(mtime), name] - - if title := title_map.get(name): - output.append("# " + title) - - print(*output, sep="\t") - -if write_title_map: - os.makedirs(later_cache_dir, exist_ok=True) - - with open(title_map_file, "w") as handle: - json.dump(title_map, handle) +title_map = TitleMap(title_map_file, update=args.update_titles) + +for entry in entries(): + print(entry) + +title_map.maybe_commit() |