@@ -25,6 +25,41 @@ class EnrichmentQueryMixin:
2525 Requires ConnectionBase attributes: conn.
2626 """
2727
28+ # ── Shared query helpers ─────────────────────────────────────────────
29+
30+ def _get_apps_without_join (self , table : str ) -> list [tuple [int , str ]]:
31+ """Returns game-type apps with no entry in the given table.
32+
33+ Args:
34+ table: Target table name to LEFT JOIN against.
35+
36+ Returns:
37+ List of (app_id, name) tuples.
38+ """
39+ cursor = self .conn .execute (
40+ f"SELECT g.app_id, g.name FROM games g"
41+ f" LEFT JOIN { table } t ON g.app_id = t.app_id"
42+ f" WHERE t.app_id IS NULL AND g.app_type IN ('game', '')"
43+ )
44+ return [(row [0 ], row [1 ]) for row in cursor .fetchall ()]
45+
46+ def _get_stale_count (self , table : str , max_age_days : int ) -> int :
47+ """Counts entries with cache older than max_age_days.
48+
49+ Args:
50+ table: Table name with a last_updated column.
51+ max_age_days: Maximum cache age in days.
52+
53+ Returns:
54+ Number of stale entries.
55+ """
56+ cutoff = int (time .time ()) - (max_age_days * 86400 )
57+ cursor = self .conn .execute (
58+ f"SELECT COUNT(*) FROM { table } WHERE last_updated < ?" ,
59+ (cutoff ,),
60+ )
61+ return cursor .fetchone ()[0 ]
62+
2863 # ── Metadata enrichment ──────────────────────────────────────────────
2964
3065 def upsert_game_metadata (self , app_id : int , ** fields : Any ) -> None :
@@ -130,17 +165,8 @@ def get_apps_missing_metadata(self) -> list[tuple[int, str]]:
130165 return [(row [0 ], row [1 ]) for row in cursor .fetchall ()]
131166
132167 def get_apps_without_hltb (self ) -> list [tuple [int , str ]]:
133- """Returns game-type apps that have no HLTB data.
134-
135- Returns:
136- List of (app_id, name) tuples.
137- """
138- cursor = self .conn .execute ("""
139- SELECT g.app_id, g.name FROM games g
140- LEFT JOIN hltb_data h ON g.app_id = h.app_id
141- WHERE h.app_id IS NULL AND g.app_type IN ('game', '')
142- """ )
143- return [(row [0 ], row [1 ]) for row in cursor .fetchall ()]
168+ """Returns game-type apps that have no HLTB data."""
169+ return self ._get_apps_without_join ("hltb_data" )
144170
145171 # ── HLTB ID cache ────────────────────────────────────────────────────
146172
@@ -254,17 +280,8 @@ def upsert_protondb(
254280 )
255281
256282 def get_apps_without_protondb (self ) -> list [tuple [int , str ]]:
257- """Returns game-type apps that have no ProtonDB rating.
258-
259- Returns:
260- List of (app_id, name) tuples.
261- """
262- cursor = self .conn .execute ("""
263- SELECT g.app_id, g.name FROM games g
264- LEFT JOIN protondb_ratings p ON g.app_id = p.app_id
265- WHERE p.app_id IS NULL AND g.app_type IN ('game', '')
266- """ )
267- return [(row [0 ], row [1 ]) for row in cursor .fetchall ()]
283+ """Returns game-type apps that have no ProtonDB rating."""
284+ return self ._get_apps_without_join ("protondb_ratings" )
268285
269286 def get_apps_without_pegi (self ) -> list [tuple [int , str ]]:
270287 """Returns game-type apps that have no PEGI age rating.
@@ -355,64 +372,22 @@ def upsert_achievements(self, app_id: int, achievements: list[dict]) -> None:
355372 )
356373
357374 def get_apps_without_achievements (self ) -> list [tuple [int , str ]]:
358- """Returns game-type apps that have no achievement_stats entry.
359-
360- Returns:
361- List of (app_id, name) tuples.
362- """
363- cursor = self .conn .execute ("""
364- SELECT g.app_id, g.name FROM games g
365- LEFT JOIN achievement_stats a ON g.app_id = a.app_id
366- WHERE a.app_id IS NULL AND g.app_type IN ('game', '')
367- """ )
368- return [(row [0 ], row [1 ]) for row in cursor .fetchall ()]
375+ """Returns game-type apps that have no achievement_stats entry."""
376+ return self ._get_apps_without_join ("achievement_stats" )
369377
370378 # ── Health check queries ─────────────────────────────────────────────
371379
372380 def get_games_missing_artwork (self ) -> list [tuple [int , str ]]:
373- """Returns games that have no custom artwork entry.
374-
375- Returns:
376- List of (app_id, name) tuples.
377- """
378- cursor = self .conn .execute ("""
379- SELECT g.app_id, g.name FROM games g
380- LEFT JOIN custom_artwork ca ON g.app_id = ca.app_id
381- WHERE ca.app_id IS NULL AND g.app_type IN ('game', '')
382- """ )
383- return [(row [0 ], row [1 ]) for row in cursor .fetchall ()]
381+ """Returns games that have no custom artwork entry."""
382+ return self ._get_apps_without_join ("custom_artwork" )
384383
385384 def get_stale_hltb_count (self , max_age_days : int = 30 ) -> int :
386- """Counts games with HLTB cache older than max_age_days.
387-
388- Args:
389- max_age_days: Maximum cache age in days.
390-
391- Returns:
392- Number of games with stale HLTB data.
393- """
394- cutoff = int (time .time ()) - (max_age_days * 86400 )
395- cursor = self .conn .execute (
396- "SELECT COUNT(*) FROM hltb_data WHERE last_updated < ?" ,
397- (cutoff ,),
398- )
399- return cursor .fetchone ()[0 ]
385+ """Counts games with HLTB cache older than max_age_days."""
386+ return self ._get_stale_count ("hltb_data" , max_age_days )
400387
401388 def get_stale_protondb_count (self , max_age_days : int = 7 ) -> int :
402- """Counts games with ProtonDB cache older than max_age_days.
403-
404- Args:
405- max_age_days: Maximum cache age in days.
406-
407- Returns:
408- Number of games with stale ProtonDB data.
409- """
410- cutoff = int (time .time ()) - (max_age_days * 86400 )
411- cursor = self .conn .execute (
412- "SELECT COUNT(*) FROM protondb_ratings WHERE last_updated < ?" ,
413- (cutoff ,),
414- )
415- return cursor .fetchone ()[0 ]
389+ """Counts games with ProtonDB cache older than max_age_days."""
390+ return self ._get_stale_count ("protondb_ratings" , max_age_days )
416391
417392 # ── Import recording ─────────────────────────────────────────────────
418393
0 commit comments