Show Metacritic scores for free games
This commit is contained in:
@@ -59,7 +59,10 @@ def _discord_send(webhook_url, games):
|
||||
title = game.get("title") or "Unknown"
|
||||
platform = SOURCE_LABELS.get(game.get("platform"), game.get("platform"))
|
||||
store_url = game.get("store_url") or ""
|
||||
score = int(game.get("metacritic_score") or 0)
|
||||
line = f"**{title}** ({platform})"
|
||||
if score > 0:
|
||||
line += f" · MC {score}"
|
||||
if store_url:
|
||||
line += f"\n{store_url}"
|
||||
lines.append(line)
|
||||
@@ -73,7 +76,10 @@ def _telegram_send(bot_token, chat_id, games):
|
||||
title = game.get("title") or "Unknown"
|
||||
platform = SOURCE_LABELS.get(game.get("platform"), game.get("platform"))
|
||||
store_url = game.get("store_url") or ""
|
||||
score = int(game.get("metacritic_score") or 0)
|
||||
line = f"<b>{title}</b> ({platform})"
|
||||
if score > 0:
|
||||
line += f" · MC {score}"
|
||||
if store_url:
|
||||
line += f"\n{store_url}"
|
||||
lines.append(line)
|
||||
@@ -109,6 +115,7 @@ class Logic(PluginModuleBase):
|
||||
super().__init__(PM, name="main", first_menu="setting")
|
||||
|
||||
def plugin_load(self):
|
||||
ModelFreeGameItem.ensure_schema()
|
||||
if _truthy(ModelSetting.get("auto_start")):
|
||||
self.scheduler_start()
|
||||
|
||||
@@ -141,6 +148,7 @@ class Logic(PluginModuleBase):
|
||||
threading.Thread(target=self.scheduler_function, daemon=True).start()
|
||||
return jsonify({"ret": "success"})
|
||||
if sub == "web_list":
|
||||
ModelFreeGameItem.ensure_schema()
|
||||
return jsonify(ModelFreeGameItem.web_list(req))
|
||||
if sub == "platform_counts":
|
||||
return jsonify({"ret": "success", "data": ModelFreeGameItem.get_platform_counts()})
|
||||
@@ -171,6 +179,7 @@ class Logic(PluginModuleBase):
|
||||
logger.info("FreeGame fetch skipped: already running")
|
||||
return
|
||||
try:
|
||||
ModelFreeGameItem.ensure_schema()
|
||||
enabled_sources = set(_enabled_sources())
|
||||
results = scraper.fetch_all()
|
||||
grouped = _split_source_payload(results)
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy import text
|
||||
|
||||
from .setup import *
|
||||
|
||||
@@ -30,6 +31,8 @@ class ModelFreeGameItem(ModelBase):
|
||||
free_end = db.Column(db.String)
|
||||
rating = db.Column(db.Float)
|
||||
rating_count = db.Column(db.Integer)
|
||||
metacritic_score = db.Column(db.Integer)
|
||||
metacritic_url = db.Column(db.String)
|
||||
genres_json = db.Column(db.Text)
|
||||
|
||||
def __init__(self):
|
||||
@@ -60,6 +63,8 @@ class ModelFreeGameItem(ModelBase):
|
||||
"free_end": self.free_end,
|
||||
"rating": self.rating,
|
||||
"rating_count": self.rating_count,
|
||||
"metacritic_score": self.metacritic_score,
|
||||
"metacritic_url": self.metacritic_url,
|
||||
"genres": self.genres,
|
||||
"updated_time": self.updated_time.strftime("%Y-%m-%d %H:%M:%S") if self.updated_time else "",
|
||||
}
|
||||
@@ -85,10 +90,30 @@ class ModelFreeGameItem(ModelBase):
|
||||
row.free_end = str(data.get("free_end") or "")
|
||||
row.rating = float(data.get("rating") or 0)
|
||||
row.rating_count = int(data.get("rating_count") or 0)
|
||||
row.metacritic_score = int(data.get("metacritic_score") or 0)
|
||||
row.metacritic_url = str(data.get("metacritic_url") or "")
|
||||
row.genres_json = json.dumps(data.get("genres") or [], ensure_ascii=False)
|
||||
row.updated_time = datetime.now()
|
||||
return row
|
||||
|
||||
@classmethod
|
||||
def ensure_schema(cls):
|
||||
with F.app.app_context():
|
||||
try:
|
||||
try:
|
||||
engine = F.db.get_engine(F.app, bind=cls.__bind_key__)
|
||||
except TypeError:
|
||||
engine = F.db.engines[cls.__bind_key__]
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(text(f"PRAGMA table_info({cls.__tablename__})")).fetchall()
|
||||
columns = {row[1] for row in rows}
|
||||
if "metacritic_score" not in columns:
|
||||
conn.execute(text(f"ALTER TABLE {cls.__tablename__} ADD COLUMN metacritic_score INTEGER"))
|
||||
if "metacritic_url" not in columns:
|
||||
conn.execute(text(f"ALTER TABLE {cls.__tablename__} ADD COLUMN metacritic_url VARCHAR"))
|
||||
except Exception:
|
||||
P.logger.exception("ff_freegame schema migration failed")
|
||||
|
||||
@classmethod
|
||||
def delete_not_in_sources(cls, sources):
|
||||
with F.app.app_context():
|
||||
|
||||
+62
-1
@@ -47,6 +47,48 @@ _STORE_MAP = {
|
||||
"30": "indiegala",
|
||||
}
|
||||
_CS_DEDICATED_STORE_IDS = {"1", "7", "30"}
|
||||
_MC_CACHE = {}
|
||||
|
||||
|
||||
def _metacritic_from_deal(d):
|
||||
try:
|
||||
score = int(float(d.get("metacriticScore") or 0))
|
||||
except Exception:
|
||||
score = 0
|
||||
link = d.get("metacriticLink") or ""
|
||||
if link and link.startswith("/"):
|
||||
link = "https://www.metacritic.com" + link
|
||||
return score, link
|
||||
|
||||
|
||||
def _cheapshark_metacritic_lookup(title="", steam_appid=None):
|
||||
key = f"{steam_appid or ''}:{title or ''}".lower()
|
||||
if key in _MC_CACHE:
|
||||
return _MC_CACHE[key]
|
||||
params = {"pageSize": 5}
|
||||
if steam_appid:
|
||||
params["steamAppID"] = steam_appid
|
||||
elif title:
|
||||
params["title"] = title
|
||||
else:
|
||||
_MC_CACHE[key] = (0, "")
|
||||
return _MC_CACHE[key]
|
||||
try:
|
||||
deals = _get(f"{_CS_BASE}/deals", params=params).json()
|
||||
normalized = re.sub(r"\s+", " ", str(title or "").strip()).lower()
|
||||
best = None
|
||||
for deal in deals or []:
|
||||
deal_title = re.sub(r"\s+", " ", str(deal.get("title") or "").strip()).lower()
|
||||
if steam_appid or deal_title == normalized:
|
||||
best = deal
|
||||
break
|
||||
if best is None and deals:
|
||||
best = deals[0]
|
||||
_MC_CACHE[key] = _metacritic_from_deal(best or {})
|
||||
except Exception as e:
|
||||
log.debug("CheapShark metacritic lookup failed title=%s appid=%s: %s", title, steam_appid, e)
|
||||
_MC_CACHE[key] = (0, "")
|
||||
return _MC_CACHE[key]
|
||||
|
||||
|
||||
def _cs_deal(d, override_platform=None):
|
||||
@@ -75,6 +117,7 @@ def _cs_deal(d, override_platform=None):
|
||||
deal_id = d.get("dealID", "")
|
||||
game_id = d.get("gameID", "")
|
||||
title = d.get("title", "")
|
||||
mc_score, mc_url = _metacritic_from_deal(d)
|
||||
is_free_period = curr == 0.0 and orig > 0.0
|
||||
return {
|
||||
"external_id": f"cs_{game_id}_{store_id}",
|
||||
@@ -91,6 +134,8 @@ def _cs_deal(d, override_platform=None):
|
||||
"genres": [],
|
||||
"rating": rating,
|
||||
"rating_count": rc,
|
||||
"metacritic_score": mc_score,
|
||||
"metacritic_url": mc_url,
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +268,7 @@ def _steam_game_dict(appid: int, detail: dict):
|
||||
is_free_period = (original > 0.0 and (current == 0.0 or discount == 100)) or free_weekend
|
||||
if not is_free_period:
|
||||
return None
|
||||
mc_score, mc_url = _cheapshark_metacritic_lookup(detail.get("name", ""), steam_appid=appid)
|
||||
|
||||
return {
|
||||
"external_id": f"steam_{appid}",
|
||||
@@ -237,6 +283,8 @@ def _steam_game_dict(appid: int, detail: dict):
|
||||
"free_start": None, "free_end": None,
|
||||
"genres": genres,
|
||||
"rating": 0.0, "rating_count": rec.get("total", 0),
|
||||
"metacritic_score": mc_score,
|
||||
"metacritic_url": mc_url,
|
||||
}
|
||||
|
||||
|
||||
@@ -965,8 +1013,18 @@ def _fetch_stove_deals_legacy():
|
||||
return results
|
||||
|
||||
|
||||
def _enrich_metacritic(items):
|
||||
for item in items or []:
|
||||
if int(item.get("metacritic_score") or 0) > 0:
|
||||
continue
|
||||
score, url = _cheapshark_metacritic_lookup(item.get("title") or "")
|
||||
item["metacritic_score"] = score
|
||||
item["metacritic_url"] = url
|
||||
return items
|
||||
|
||||
|
||||
def fetch_all():
|
||||
return {
|
||||
data = {
|
||||
"epic": fetch_epic_free(),
|
||||
"steam": fetch_steam_free(),
|
||||
"cheapshark": fetch_cheapshark_deals(),
|
||||
@@ -975,6 +1033,9 @@ def fetch_all():
|
||||
"indiegala_free": fetch_indiegala_free(),
|
||||
"stove": fetch_stove_deals(),
|
||||
}
|
||||
for items in data.values():
|
||||
_enrich_metacritic(items)
|
||||
return data
|
||||
|
||||
|
||||
def _parse_dt(s):
|
||||
|
||||
@@ -145,11 +145,31 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
.ffg-rating {
|
||||
min-width: 54px;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
min-width: 74px;
|
||||
text-align: right;
|
||||
color: #8e96c4;
|
||||
font-size: 12px;
|
||||
}
|
||||
.ffg-mc {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 7px;
|
||||
border-radius: 8px;
|
||||
background: rgba(250, 204, 21, .14);
|
||||
border: 1px solid rgba(250, 204, 21, .32);
|
||||
color: #fde68a;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
.ffg-steam-rating {
|
||||
color: #8e96c4;
|
||||
line-height: 1;
|
||||
}
|
||||
.ffg-empty {
|
||||
padding: 48px 16px;
|
||||
text-align: center;
|
||||
@@ -223,7 +243,11 @@ function gameCard(item) {
|
||||
const genres = Array.isArray(item.genres) ? item.genres.slice(0, 2) : [];
|
||||
const genreHtml = genres.map((genre) => `<span class="ffg-genre">${escapeHtml(genre)}</span>`).join("");
|
||||
const ratingHtml = item.rating && Number(item.rating) > 0
|
||||
? `${Math.round(Number(item.rating))}`
|
||||
? `<span class="ffg-steam-rating">Steam ${Math.round(Number(item.rating))}</span>`
|
||||
: "";
|
||||
const mcScore = Number(item.metacritic_score || 0);
|
||||
const mcHtml = mcScore > 0
|
||||
? `<span class="ffg-mc" title="Metacritic score">MC ${Math.round(mcScore)}</span>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
@@ -239,7 +263,7 @@ function gameCard(item) {
|
||||
<div class="ffg-genres">${genreHtml}</div>
|
||||
<div class="ffg-meta">
|
||||
<div class="ffg-countdown"><i class="fa fa-clock-o"></i><span>${escapeHtml(formatCountdown(item.free_end))}</span></div>
|
||||
<div class="ffg-rating">${ratingHtml}</div>
|
||||
<div class="ffg-rating">${mcHtml}${ratingHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user