2023-05-09 23:03:56 +02:00

138 lines
5.3 KiB
Python
Executable File

import logging
from dataclasses import dataclass
from datetime import datetime
from typing import Generator
import bs4
import cloudscraper
from updater.site import CURSE_UA
from updater.site.abstract_site import AbstractSite, SiteError
from updater.site.enum import AddonVersion, GameVersion
logger = logging.getLogger(__name__)
@dataclass
class CurseAddonVersion:
type: AddonVersion
name: str
size: str
uploaded: str
game_version: str
downloads: int
download_link: str
@classmethod
def from_tr(cls, tr: bs4.element.Tag):
cells = tr.find_all('td')
name = cells[1].text.strip()
name = name if '\n' not in name else name.split('\n')[0]
size = cells[2].text.strip()
uploaded = datetime.fromtimestamp(int(cells[3].find('abbr').attrs.get('data-epoch'))).isoformat()
game_version = cells[4].text.strip()
downloads = int(cells[5].text.replace(',', '').strip())
return cls(type=cls.get_type(cells[0]), name=name, size=size,
uploaded=uploaded, game_version=game_version, downloads=downloads,
download_link=cls.get_link(cells[6]))
@staticmethod
def get_type(td: bs4.element.Tag) -> AddonVersion:
class_fields = td.find('div').attrs.get('class')
bg_field = next(field for field in class_fields if field.startswith('bg-'))
if 'blue' in bg_field:
return AddonVersion.release #beta
elif 'green' in bg_field:
return AddonVersion.release
elif 'offset' in bg_field:
return AddonVersion.alpha
else:
raise ValueError
@staticmethod
def get_link(td: bs4.element.Tag) -> str:
relative_link = td.find('a').attrs.get('href')
return f'https://www.curseforge.com{relative_link}/file'
class Curse(AbstractSite):
_OLD_URL = 'https://mods.curse.com/addons/wow/'
_OLD_PROJECT_URL = 'https://wow.curseforge.com/projects/'
_URLS = [
'https://www.curseforge.com/wow/addons/',
'https://curseforge.com/wow/addons/',
_OLD_URL,
_OLD_PROJECT_URL
]
session = cloudscraper.create_scraper(browser=CURSE_UA,delay=10)
def __init__(self, url: str, game_version: GameVersion, addon_version: AddonVersion = AddonVersion.release):
url = Curse._normalize_curse_urls(url)
super().__init__(url, game_version)
self.addon_version = addon_version
def find_zip_url(self):
try:
return self.latest_release().download_link
except Exception as e:
raise self.download_error() from e
def versions(self, *, page=1) -> Generator[CurseAddonVersion, None, None]:
"""Yields a sequence of CurseAddonVersions corresponding to addon releases
Ordered descending in time, so the first version yielded is the most recent.
Will page through until exhausted.
"""
if self.game_version == GameVersion.classic:
game_version_filter = '1738749986:67408'
elif self.game_version == GameVersion.retail:
game_version_filter = '1738749986:517'
else: # Agnostic version
game_version_filter = ''
request_params = {'filter-game-version': game_version_filter, 'page': page}
try:
p = Curse.session.get(f'{self.url}/files/all', params=request_params)
soup = bs4.BeautifulSoup(p.text, 'html.parser')
versions_table = soup.find('table', {'class': 'listing listing-project-file project-file-listing b-table b-table-a'})
# Header row consumed by _
_, *version_rows = versions_table.find_all('tr')
yield from (CurseAddonVersion.from_tr(row) for row in version_rows)
# determine if there are more pages of versions, recurse if so
pages_exist = soup.find('div', {'class': 'pagination pagination-top flex items-center'})
inactive_next_page = soup.find('div', {'class': 'pagination-next h-6 w-6 flex items-center justify-center pagination-next--inactive'})
if pages_exist and not inactive_next_page:
yield from self.versions(page=page+1)
except Exception as e:
raise self.version_error() from e
def latest_release(self) -> CurseAddonVersion:
latest = next(version for version in self.versions() if version.type >= self.addon_version)
return latest
def get_latest_version(self) -> str:
"""Returns the latest version released for retail/classic
The `version.type >= self.addon_version` logic chooses the most recent
addon according to the ordering that release > beta > alpha. So if you
are following the beta track and a new alpha version is release, you won't
get it, but a new release version you will.
Returns the name of the most recent release.
"""
return self.latest_release().name
@classmethod
def _normalize_curse_urls(cls, url: str) -> str:
try:
# Some old URLs may point to nonexistent pages. Rather than guess at what the new
# name and URL is, just try to load the old URL and see where Curse redirects us to.
page = Curse.session.get(url)
page.raise_for_status()
return page.url
except Exception as e:
raise SiteError(f"Failed to find the current page for old URL: {url}") from e