Adds a comic format registry
There is the limitation that you still need to manually import all modules that register a new comic format, but this does give us a lot of flexibility
This commit is contained in:
@@ -6,6 +6,7 @@ import argparse
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
import bisect
|
||||
import time
|
||||
import re
|
||||
import textwrap
|
||||
@@ -252,6 +253,9 @@ class Comic(ImageRepo, ABC):
|
||||
"""
|
||||
A generic representation of a comic collection. Comic-wide resources are stored here.
|
||||
"""
|
||||
|
||||
strip_cls = None
|
||||
"""A reference to the class used for this comic's strips."""
|
||||
|
||||
def __init__(self, identifier):
|
||||
"""
|
||||
@@ -265,6 +269,14 @@ class Comic(ImageRepo, ABC):
|
||||
self.author = None
|
||||
"""The author(s) of the comic. Can be a string for a name, a list of names, or a list of objects containing the names under "name" and role under "role"."""
|
||||
|
||||
#@abstractmethod
|
||||
@classmethod
|
||||
def create_from_url(cls, url):
|
||||
"""
|
||||
Creates a Comic instance from a given URL
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _load_data(self):
|
||||
"""
|
||||
@@ -277,7 +289,35 @@ class Comic(ImageRepo, ABC):
|
||||
relevant images, loaded into `image_urls`.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_first_strip(self):
|
||||
"""
|
||||
Obtains the identifier of the first strip.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_latest_strip(self):
|
||||
"""
|
||||
Obtains the identifier of the latest strip.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_all_strips(self) -> list:
|
||||
"""
|
||||
Obtains a list of all strips.
|
||||
"""
|
||||
pass
|
||||
|
||||
def create_strip(self, identifier):
|
||||
"""
|
||||
Creates a new ComicStrip object.
|
||||
|
||||
The class variable #strip_cls must be set for this to work.
|
||||
"""
|
||||
return self.strip_cls(self, identifier)
|
||||
|
||||
|
||||
class ComicStrip(ImageRepo, ABC):
|
||||
@@ -427,3 +467,149 @@ class ComicStrip(ImageRepo, ABC):
|
||||
image_path = Path(image.name)
|
||||
if image_path.exists():
|
||||
image_path.unlink()
|
||||
|
||||
class URLFormat:
|
||||
"""An internal class used to help sort URLs based on subdomains and paths"""
|
||||
def __init__(self, url: str):
|
||||
self.raw_url = url
|
||||
if not re.match(r'^\w+://', url):
|
||||
url = 'http://' + url
|
||||
self.url = urlparse(url)
|
||||
self.domains = self.url.netloc.split('.')
|
||||
self.paths = self.url.path.split('/')
|
||||
|
||||
def __lt__(self, other):
|
||||
if type(other) == str:
|
||||
other = URLFormat(other)
|
||||
|
||||
if len(self.domains) != len(other.domains):
|
||||
return len(self.domains) < len(other.domains)
|
||||
if self.domains != other.domains:
|
||||
return self.domains < other.domains
|
||||
if len(self.paths) != len(other.paths):
|
||||
return len(self.paths) < len(other.paths)
|
||||
if self.paths != other.paths:
|
||||
return self.paths < other.paths
|
||||
return False
|
||||
|
||||
def __le__(self, other):
|
||||
if type(other) == str:
|
||||
other = URLFormat(other)
|
||||
|
||||
if self == other:
|
||||
return True
|
||||
return self < other
|
||||
|
||||
def __gt__(self, other):
|
||||
if type(other) == str:
|
||||
other = URLFormat(other)
|
||||
|
||||
if len(self.domains) != len(other.domains):
|
||||
return len(self.domains) > len(other.domains)
|
||||
if self.domains != other.domains:
|
||||
return self.domains > other.domains
|
||||
if len(self.paths) != len(other.paths):
|
||||
return len(self.paths) > len(other.paths)
|
||||
if self.paths != other.paths:
|
||||
return self.paths > other.paths
|
||||
return False
|
||||
|
||||
def __ge__(self, other):
|
||||
if type(other) == str:
|
||||
other = URLFormat(other)
|
||||
|
||||
if self == other:
|
||||
return True
|
||||
return self > other
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) == str:
|
||||
other = URLFormat(other)
|
||||
|
||||
return self.domains == other.domains and self.paths == other.paths
|
||||
|
||||
def __ne__(self, other):
|
||||
if type(other) == str:
|
||||
other = URLFormat(other)
|
||||
|
||||
return self.domains != other.domains or self.paths != other.paths
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.url.netloc}{self.url.path}"
|
||||
|
||||
def __repr__(self):
|
||||
return f"\"{self.__str__()}\""
|
||||
|
||||
def __hash__(self):
|
||||
return hash(tuple(self.domains + self.paths))
|
||||
|
||||
comic_registry = {}
|
||||
"""A registry that maps URL Domains to Comic classes."""
|
||||
|
||||
_registry_subdomains = []
|
||||
"""An internal list keeping track of the order we should use to find the right parser (priority is last)"""
|
||||
|
||||
def register_comic_class(url: str, comic_cls: type[Comic]):
|
||||
"""
|
||||
Registers a comic type.
|
||||
|
||||
The URL format provided here is used to help determine
|
||||
which class should be used for a given strip. There are
|
||||
a few rules for the URL parameter provided here:
|
||||
|
||||
1. The url should not include the schema (i.e. "http://").
|
||||
For example, "xkcd.com" is okay, but "https://xkcd.com" is
|
||||
not.
|
||||
2. The URL provided here should usually just express the
|
||||
domain name. Paths should only be expressed if absolutely
|
||||
necessary, such as in cases where different comic formats
|
||||
are published to the same site (i.e. Tumblr). For example,
|
||||
"bsky.app" works, but "bsky.app/profile" is discourages
|
||||
(although "bsky.app/profile/strange_user.bsky.social"
|
||||
is acceptable if strange_user does something unusual
|
||||
with the way he publishes his comics and needs a custom
|
||||
parser).
|
||||
3. If the subdomain is "www", it should be ommited.
|
||||
Use "tumblr.com" instead of "www.tumblr.com" (although
|
||||
"john_doe.tumblr.com" is valid).
|
||||
|
||||
:param url: The URL format to register.
|
||||
:param comic_cls: The class to use for parsing this comic.
|
||||
"""
|
||||
url_f = URLFormat(url)
|
||||
if url_f in comic_registry:
|
||||
raise ValueError("URL %s is already registered" % url)
|
||||
comic_registry[url_f] = comic_cls
|
||||
bisect.insort(_registry_subdomains, url_f)
|
||||
|
||||
def find_comic_class(url: str) -> type[Comic]:
|
||||
"""
|
||||
Finds the appropriate comic for a given URL.
|
||||
|
||||
:param url: The URL of the comic to parse. This must be a proper URL (including schema, etc.)
|
||||
:returns: A parser
|
||||
"""
|
||||
url_f = URLFormat(url)
|
||||
logging.info(_registry_subdomains)
|
||||
while len(url_f.domains) > 0:
|
||||
i = bisect.bisect(_registry_subdomains, url_f)
|
||||
if i == 0:
|
||||
raise ValueError("Could not find a parser for url \"%s\"" % url)
|
||||
if url_f.domains == _registry_subdomains[i - 1].domains:
|
||||
# We have found a series of parsers with a matching subdomain.
|
||||
# Now we check the paths.
|
||||
while len(url_f.paths) > 0:
|
||||
i = bisect.bisect(_registry_subdomains, url_f)
|
||||
if i == 0:
|
||||
raise ValueError("Could not find a parser for url \"%s\"" % url)
|
||||
if url_f.paths == _registry_subdomains[i - 1].paths:
|
||||
# We have found a registered URL pattern that matches the URL
|
||||
# provided.
|
||||
return comic_registry[url_f]
|
||||
else:
|
||||
# Drill down to find an exact matching path
|
||||
url_f = URLFormat('.'.join(url_f.domains) + '/'.join(url_f.paths[:-1]))
|
||||
else:
|
||||
# The result we found has a different subdomain. Look for the root subdomain
|
||||
url_f = URLFormat('.'.join(url_f.domains[1:]) + '/'.join(url_f.paths))
|
||||
raise ValueError("Could not find a parse for url \"%s\"" % url)
|
||||
|
||||
Reference in New Issue
Block a user