Add support for different game versions

This commit is contained in:
Casper V. Kristensen 2024-01-06 03:24:00 +01:00
parent 0086cb798c
commit 5ab103df58
9 changed files with 65 additions and 38 deletions

View file

@ -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)

View file

@ -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()

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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:

View file

@ -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:

View file

@ -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}/")