Add support for different game versions
This commit is contained in:
parent
0086cb798c
commit
5ab103df58
|
@ -5,6 +5,7 @@ import logging
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, List, Dict
|
from typing import Iterable, List, Dict
|
||||||
|
|
||||||
|
@ -13,6 +14,12 @@ from . import config, providers
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Game(Enum):
|
||||||
|
WOTLK = "wotlk"
|
||||||
|
ERA = "era"
|
||||||
|
RETAIL = "retail"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Addon:
|
class Addon:
|
||||||
name: str = None
|
name: str = None
|
||||||
|
@ -64,12 +71,12 @@ class Addon:
|
||||||
return provider
|
return provider
|
||||||
raise ValueError(f"No AddOn provider for {self}")
|
raise ValueError(f"No AddOn provider for {self}")
|
||||||
|
|
||||||
def download(self) -> bool:
|
def download(self, game: Game) -> bool:
|
||||||
try:
|
try:
|
||||||
logger.info("Downloading %s", self.url)
|
logger.info("Downloading %s", self.url)
|
||||||
self.create_download_dir()
|
self.create_download_dir()
|
||||||
provider = self.get_provider()
|
provider = self.get_provider()
|
||||||
changed = provider.download(self)
|
changed = provider.download(self, game)
|
||||||
return changed
|
return changed
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
12
wau/cli.py
12
wau/cli.py
|
@ -5,7 +5,7 @@ import shutil
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from . import __url__, __author__, __version__, config, providers, addons
|
from . import __url__, __author__, __version__, config, providers, addons
|
||||||
from .addons import Addon
|
from .addons import Addon, Game
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -28,6 +28,12 @@ class CLI:
|
||||||
epilog=f"For more information, see <{__url__}>."
|
epilog=f"For more information, see <{__url__}>."
|
||||||
)
|
)
|
||||||
parser.set_defaults(func=lambda a: parser.print_help())
|
parser.set_defaults(func=lambda a: parser.print_help())
|
||||||
|
parser.add_argument(
|
||||||
|
"-g", "--game",
|
||||||
|
choices=[g.value for g in Game],
|
||||||
|
default=Game.WOTLK.value,
|
||||||
|
help="Game version. Used to select the optimal AddOn in case there are multiple available versions."
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-v", "--verbose",
|
"-v", "--verbose",
|
||||||
action="count",
|
action="count",
|
||||||
|
@ -105,7 +111,7 @@ class CLI:
|
||||||
continue
|
continue
|
||||||
print(f"Installing AddOn from {url}")
|
print(f"Installing AddOn from {url}")
|
||||||
addon = Addon(url=url)
|
addon = Addon(url=url)
|
||||||
addon.download()
|
addon.download(game=Game(args.game))
|
||||||
addon.install()
|
addon.install()
|
||||||
self.installed_addons.append(addon)
|
self.installed_addons.append(addon)
|
||||||
addons.save_installed_addons(self.installed_addons)
|
addons.save_installed_addons(self.installed_addons)
|
||||||
|
@ -122,7 +128,7 @@ class CLI:
|
||||||
def update(self, args) -> None:
|
def update(self, args) -> None:
|
||||||
for addon in self.installed_addons:
|
for addon in self.installed_addons:
|
||||||
print(f"Updating {addon.name}..", end=" ")
|
print(f"Updating {addon.name}..", end=" ")
|
||||||
changed = addon.download()
|
changed = addon.download(game=Game(args.game))
|
||||||
if changed:
|
if changed:
|
||||||
addon.uninstall()
|
addon.uninstall()
|
||||||
addon.install()
|
addon.install()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from ..addons import Addon
|
from ..addons import Addon, Game
|
||||||
|
|
||||||
|
|
||||||
class Provider:
|
class Provider:
|
||||||
|
@ -10,7 +10,7 @@ class Provider:
|
||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download(cls, addon: Addon) -> bool:
|
def download(cls, addon: Addon, game: Game) -> bool:
|
||||||
"""
|
"""
|
||||||
Download the provided AddOn.
|
Download the provided AddOn.
|
||||||
Returns True if the AddOn data was changed (i.e. downloaded/updated), False otherwise.
|
Returns True if the AddOn data was changed (i.e. downloaded/updated), False otherwise.
|
||||||
|
|
|
@ -5,7 +5,7 @@ from typing import Dict
|
||||||
|
|
||||||
from .web import Web
|
from .web import Web
|
||||||
from .. import http
|
from .. import http
|
||||||
from ..addons import Addon
|
from ..addons import Addon, Game
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -27,12 +27,12 @@ class CurseForge(Web):
|
||||||
return "curseforge.com/wow/addons/" in url
|
return "curseforge.com/wow/addons/" in url
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download(cls, addon: Addon, url: str = None) -> bool:
|
def download(cls, addon: Addon, game: Game, url: str = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Twitch API from: https://github.com/Gaz492/TwitchAPI. Thanks Gareth! <3
|
Twitch API from: https://github.com/Gaz492/TwitchAPI. Thanks Gareth! <3
|
||||||
"""
|
"""
|
||||||
latest_file_url = cls._get_latest_file_url(addon)
|
latest_file_url = cls._get_latest_file_url(addon, game)
|
||||||
return super().download(addon, url=latest_file_url)
|
return super().download(addon, game, url=latest_file_url)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_curse_id(cls, addon: Addon) -> int:
|
def _get_curse_id(cls, addon: Addon) -> int:
|
||||||
|
@ -71,14 +71,23 @@ class CurseForge(Web):
|
||||||
raise ValueError("AddOn slug not found in CurseForge search results.")
|
raise ValueError("AddOn slug not found in CurseForge search results.")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_latest_file_url(cls, addon: Addon) -> str:
|
def _game_version_priorities(cls, game: Game) -> list[int]:
|
||||||
|
wotlk, tbc, era, retail = 73713, 73246, 67408, 517 # https://api.curseforge.com/v1/games/1/version-types
|
||||||
|
priorities = {
|
||||||
|
game.WOTLK: [wotlk, era, tbc, retail],
|
||||||
|
game.ERA: [era, wotlk, tbc, retail],
|
||||||
|
game.RETAIL: [retail, wotlk, era, tbc],
|
||||||
|
}
|
||||||
|
return priorities[game]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_latest_file_url(cls, addon: Addon, game: Game) -> str:
|
||||||
curse_id = cls._get_curse_id(addon)
|
curse_id = cls._get_curse_id(addon)
|
||||||
logger.debug("Getting latest AddOn info from CurseForge")
|
logger.debug("Getting latest AddOn info from CurseForge")
|
||||||
files = http.open(f"{cls.api_url}/v1/mods/{curse_id}/files", headers=cls.headers()).json()["data"]
|
files = http.open(f"{cls.api_url}/v1/mods/{curse_id}/files", headers=cls.headers()).json()["data"]
|
||||||
|
version_priorities = cls._game_version_priorities(game)
|
||||||
|
|
||||||
def key(index: int, file: dict) -> tuple:
|
def key(index: int, file: dict) -> tuple:
|
||||||
# https://api.curseforge.com/v1/games/1/version-types: [WotLK, TBC, Classic, Retail]
|
|
||||||
version_priorities = [73713, 73246, 67408, 517]
|
|
||||||
return (
|
return (
|
||||||
min(version_priorities.index(v["gameVersionTypeId"]) for v in file["sortableGameVersions"]),
|
min(version_priorities.index(v["gameVersionTypeId"]) for v in file["sortableGameVersions"]),
|
||||||
file["releaseType"], # releaseType: 1: release, 2: beta, 3: alpha
|
file["releaseType"], # releaseType: 1: release, 2: beta, 3: alpha
|
||||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from .base import Provider
|
from .base import Provider
|
||||||
from ..addons import Addon
|
from ..addons import Addon, Game
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class Git(Provider):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download(cls, addon: Addon) -> bool:
|
def download(cls, addon: Addon, game: Game) -> bool:
|
||||||
if addon.is_cached():
|
if addon.is_cached():
|
||||||
return cls._pull(addon)
|
return cls._pull(addon)
|
||||||
cls._clone(addon)
|
cls._clone(addon)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from typing import Tuple
|
||||||
|
|
||||||
from .web import Web
|
from .web import Web
|
||||||
from .. import http
|
from .. import http
|
||||||
from ..addons import Addon
|
from ..addons import Addon, Game
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -22,24 +22,29 @@ class GitHub(Web):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download(cls, addon: Addon, url: str = None) -> bool:
|
def _asset_priorities(cls, game: Game) -> list[str]:
|
||||||
|
wotlk = ["wotlk", "wrath"]
|
||||||
|
era = ["era", "classic", "vanilla"]
|
||||||
|
retail = ["retail", "mainline"]
|
||||||
|
wildcard = [".*"]
|
||||||
|
priorities = {
|
||||||
|
game.WOTLK: wotlk + era + retail,
|
||||||
|
game.ERA: era + wotlk + retail,
|
||||||
|
game.RETAIL: retail,
|
||||||
|
}
|
||||||
|
priority = priorities[game]
|
||||||
|
priority_patterns = [f"[._-]{p}[._-]" for p in priority]
|
||||||
|
return priority_patterns + wildcard
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def download(cls, addon: Addon, game: Game, url: str = None) -> bool:
|
||||||
repo_owner, repo_name = cls._parse_url(addon.url)
|
repo_owner, repo_name = cls._parse_url(addon.url)
|
||||||
addon.name = repo_name
|
addon.name = repo_name
|
||||||
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()
|
||||||
|
|
||||||
asset_priorities = [
|
asset_priorities = cls._asset_priorities(game)
|
||||||
r"[._-]wotlk[._-]",
|
|
||||||
r"[._-]wrath[._-]",
|
|
||||||
r"[._-]tbc[._-]",
|
|
||||||
r"[._-]tbcc[._-]",
|
|
||||||
r"[._-]bcc[._-]",
|
|
||||||
r"[._-]bc[._-]",
|
|
||||||
r"[._-]classic[._-]",
|
|
||||||
r"[._-]vanilla[._-]",
|
|
||||||
r".*",
|
|
||||||
]
|
|
||||||
for asset in sorted(
|
for asset in sorted(
|
||||||
latest_release["assets"],
|
latest_release["assets"],
|
||||||
key=lambda a: next(
|
key=lambda a: next(
|
||||||
|
@ -49,7 +54,7 @@ class GitHub(Web):
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
if Path(asset["name"]).suffix == ".zip":
|
if Path(asset["name"]).suffix == ".zip":
|
||||||
return super().download(addon, asset["browser_download_url"])
|
return super().download(addon, game, url=asset["browser_download_url"])
|
||||||
raise FileNotFoundError("No zip file found for latest release")
|
raise FileNotFoundError("No zip file found for latest release")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
|
|
||||||
from .web import Web
|
from .web import Web
|
||||||
from .. import http
|
from .. import http
|
||||||
from ..addons import Addon
|
from ..addons import Addon, Game
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -15,9 +15,9 @@ class TukUI(Web):
|
||||||
return "tukui.org" in url
|
return "tukui.org" in url
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download(cls, addon: Addon, url: str = None) -> bool:
|
def download(cls, addon: Addon, game: Game, url: str = None) -> bool:
|
||||||
file_url = cls._get_file_url(addon)
|
file_url = cls._get_file_url(addon)
|
||||||
return super().download(addon, url=file_url)
|
return super().download(addon, game, url=file_url)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_file_url(self, addon: Addon) -> str:
|
def _get_file_url(self, addon: Addon) -> str:
|
||||||
|
|
|
@ -4,7 +4,7 @@ import urllib.request
|
||||||
|
|
||||||
from .base import Provider
|
from .base import Provider
|
||||||
from .. import http
|
from .. import http
|
||||||
from ..addons import Addon
|
from ..addons import Addon, Game
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class Web(Provider):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download(cls, addon: Addon, url: str = None) -> bool:
|
def download(cls, addon: Addon, game: Game, url: str = None) -> bool:
|
||||||
url = url or addon.url
|
url = url or addon.url
|
||||||
head = http.open(url).head()
|
head = http.open(url).head()
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .web import Web
|
from .web import Web
|
||||||
from ..addons import Addon
|
from ..addons import Addon, Game
|
||||||
|
|
||||||
|
|
||||||
class WowInterface(Web):
|
class WowInterface(Web):
|
||||||
|
@ -10,8 +10,8 @@ class WowInterface(Web):
|
||||||
return "wowinterface.com/downloads/info" in url
|
return "wowinterface.com/downloads/info" in url
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def download(cls, addon: Addon, url: str = None) -> bool:
|
def download(cls, addon: Addon, game: Game, url: str = None) -> bool:
|
||||||
addon_id, addon_name = re.search(r"info(\d+)-?(.*)", addon.url).groups()
|
addon_id, addon_name = re.search(r"info(\d+)-?(.*)", addon.url).groups()
|
||||||
if addon_name:
|
if addon_name:
|
||||||
addon.name = addon_name.replace(".html", "")
|
addon.name = addon_name.replace(".html", "")
|
||||||
return super().download(addon, url=f"https://cdn.wowinterface.com/downloads/file{addon_id}/")
|
return super().download(addon, game, url=f"https://cdn.wowinterface.com/downloads/file{addon_id}/")
|
||||||
|
|
Loading…
Reference in a new issue