Compare commits

...

3 commits

3 changed files with 39 additions and 39 deletions

View file

@ -6,7 +6,7 @@ import re
import shutil import shutil
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Iterable, List from typing import Iterable, List, Dict
from . import config, providers from . import config, providers
@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
class Addon: class Addon:
name: str = None name: str = None
url: str = None url: str = None
dirs: List[Path] = field(default_factory=list) dirs: Dict[str, Path] = field(default_factory=dict)
provider: str = None provider: str = None
provider_data: dict = field(default_factory=dict) provider_data: dict = field(default_factory=dict)
@ -26,7 +26,7 @@ class Addon:
return { return {
"name": self.name, "name": self.name,
"url": self.url, "url": self.url,
"dirs": [str(d) for d in self.dirs], "dirs": {name: str(path) for name, path in self.dirs.items()},
"provider": self.provider, "provider": self.provider,
"provider_data": self.provider_data "provider_data": self.provider_data
} }
@ -36,7 +36,7 @@ class Addon:
return cls( return cls(
name=data["name"], name=data["name"],
url=data["url"], url=data["url"],
dirs=[Path(d) for d in data["dirs"]], dirs={name: Path(path) for name, path in data["dirs"].items()},
provider=data["provider"], provider=data["provider"],
provider_data=data["provider_data"] provider_data=data["provider_data"]
) )
@ -51,7 +51,7 @@ class Addon:
self.download_dir.mkdir(parents=True, exist_ok=True) self.download_dir.mkdir(parents=True, exist_ok=True)
def is_cached(self) -> bool: def is_cached(self) -> bool:
return self.dirs and all(d.is_dir() for d in self.dirs) return self.dirs and all(d.is_dir() for d in self.dirs.values())
def get_provider(self) -> providers.Provider: def get_provider(self) -> providers.Provider:
if self.provider is not None: if self.provider is not None:
@ -74,36 +74,41 @@ class Addon:
def install(self) -> None: def install(self) -> None:
logger.info("Installing %s", self) logger.info("Installing %s", self)
self.dirs = find_addon_dirs(self.download_dir) self.dirs = find_addon_dirs(self.download_dir)
for dir in self.dirs: for dir_name, path in self.dirs.items():
shutil.copytree(str(dir), config.ADDONS_DIR.joinpath(dir.name)) shutil.copytree(str(path), config.ADDONS_DIR.joinpath(dir_name))
if self.name is None: # didn't get AddOn name from provider if self.name is None: # didn't get AddOn name from provider
if len(self.dirs) == 1: if len(self.dirs) == 1:
self.name = self.dirs[0].name self.name = next(iter(self.dirs.keys()))
else: else:
print(f"Unable to identify AddOn name for {self.url}") print(f"Unable to identify AddOn name for {self.url}")
self.name = input("AddOn name: ") self.name = input("AddOn name: ")
def uninstall(self) -> None: def uninstall(self) -> None:
logger.info("Uninstalling %s", self) logger.info("Uninstalling %s", self)
for dir in self.dirs: for dir_name in self.dirs.keys():
shutil.rmtree(config.ADDONS_DIR.joinpath(dir.name), ignore_errors=True) shutil.rmtree(config.ADDONS_DIR.joinpath(dir_name), ignore_errors=True)
def find_addon_dirs(path: Path) -> List[Path]: def find_addon_dirs(path: Path) -> Dict[str, Path]:
""" """
Find and return AddOns in the given path. WoW AddOns are identified by a .toc file whose filename matches its containing folder. The algorithm traverses the
given directory path in a breadth-first fashion, returning all AddOns in the first level which contains any such
Each WoW AddOn is identified by a .toc-file whose filename matches its containing folder. The algorithm traverses directory/.toc-pairs. This ensures that we, as an example, find both AddOns 'Foo' and 'Foo_Config'.
the path in a breadth-first fashion, returning all found AddOns in the first level which contains any AddOns. Occasionally, the downloaded directory will contain no directory/.toc-pairs which conform to the Blizzard AddOn
This ensures we find AddOns 'Foo' and 'Foo_Config'. standard; e.g. in the case of 'foo-master' from git with 'Foo.toc' in the root. Therefore, as a special case, if the
first level in the given path contains a single .toc file, this path will be returned along with its proper name, so
it can be renamed on install to Interface/AddOns.
""" """
level = [path] level = [path]
while level: while level:
addon_dirs = [] addon_dirs = {}
for p in level: for p in level:
if any(toc.stem == p.name for toc in p.glob("*.toc")): tocs = list(p.glob("*.toc"))
addon_dirs.append(p) if any(toc.stem == p.name for toc in tocs):
addon_dirs[p.name] = p
elif len(level) == 1 and len(tocs) == 1:
addon_dirs[tocs[0].stem] = p
if addon_dirs: if addon_dirs:
logger.debug("Found AddOn dirs: %s in '%s'", addon_dirs, path) logger.debug("Found AddOn dirs: %s in '%s'", addon_dirs, path)
return addon_dirs return addon_dirs
@ -126,7 +131,7 @@ def load_installed_addons() -> List[Addon]:
logger.debug("Loading list of installed AddOns from %s", config.CONFIG_FILE) logger.debug("Loading list of installed AddOns from %s", config.CONFIG_FILE)
try: try:
with config.CONFIG_FILE.open() as cf: with config.CONFIG_FILE.open() as cf:
return [Addon.from_json(a) for a in json.load(cf)["installed_addons"]] return [Addon.from_json(addon) for addon in json.load(cf)["installed_addons"]]
except FileNotFoundError: except FileNotFoundError:
return [] return []
@ -134,4 +139,4 @@ def load_installed_addons() -> List[Addon]:
def save_installed_addons(addons: Iterable[Addon]) -> None: def save_installed_addons(addons: Iterable[Addon]) -> None:
logger.debug("Saving list of installed AddOns to %s", config.CONFIG_FILE) logger.debug("Saving list of installed AddOns to %s", config.CONFIG_FILE)
with config.CONFIG_FILE.open("w") as cf: with config.CONFIG_FILE.open("w") as cf:
json.dump(dict(installed_addons=[a.to_json for a in addons]), cf) json.dump(dict(installed_addons=[addon.to_json for addon in addons]), cf)

View file

@ -98,7 +98,7 @@ class CLI:
args.func(args) args.func(args)
def install(self, args) -> None: def install(self, args) -> None:
already_installed_urls = {a.url for a in self.installed_addons} already_installed_urls = {addon.url for addon in self.installed_addons}
for url in set(args.urls): # set removes duplicates for url in set(args.urls): # set removes duplicates
if url in already_installed_urls: if url in already_installed_urls:
print(f"{url} is already installed") print(f"{url} is already installed")
@ -111,13 +111,8 @@ class CLI:
addons.save_installed_addons(self.installed_addons) addons.save_installed_addons(self.installed_addons)
def remove(self, args) -> None: def remove(self, args) -> None:
if args.all: for addon in self.installed_addons:
remove = self.installed_addons.copy() if args.all or addon.name in args.addons:
else:
remove = [addon
for addon in self.installed_addons
if addon.name in args.addons]
for addon in remove:
print(f"Removing {addon.name}") print(f"Removing {addon.name}")
addon.uninstall() addon.uninstall()
shutil.rmtree(addon.download_dir, ignore_errors=True) shutil.rmtree(addon.download_dir, ignore_errors=True)
@ -136,12 +131,12 @@ class CLI:
def list(self, args) -> None: def list(self, args) -> None:
table = [] table = []
for addon in self.installed_addons: for addon in sorted(self.installed_addons, key=lambda a: a.name.lower()):
table.append({ table.append({
"Name": addon.name, "Name": addon.name,
"Provider": addon.get_provider().__name__, "Provider": addon.get_provider().__name__,
"URL": addon.url, "URL": addon.url,
"Directories": textwrap.fill(", ".join(d.name for d in addon.dirs), width=40) "Directories": textwrap.fill(", ".join(addon.dirs.keys()), width=40)
}) })
from tabulate import tabulate from tabulate import tabulate
print(tabulate(table, headers="keys", tablefmt="orgtbl")) print(tabulate(table, headers="keys", tablefmt="orgtbl"))

View file

@ -28,7 +28,7 @@ class GitHub(Web):
latest_release = http.open( latest_release = http.open(
url=f"{cls.api_url}/repos/{repo_owner}/{repo_name}/releases/latest" url=f"{cls.api_url}/repos/{repo_owner}/{repo_name}/releases/latest"
).json() ).json()
for asset in latest_release["assets"]: for asset in sorted(latest_release["assets"], key=lambda a: "classic" in a["name"].lower(), reverse=True):
if Path(asset["name"]).suffix == ".zip": if Path(asset["name"]).suffix == ".zip":
return super().download(addon, asset["browser_download_url"]) return super().download(addon, asset["browser_download_url"])
raise FileNotFoundError("No zip file found for latest release") raise FileNotFoundError("No zip file found for latest release")