|
| 1 | +# Database Schema Documentation |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Backlogia uses SQLite as its database engine. The database consolidates game libraries from multiple stores (Steam, Epic, GOG, itch.io, Humble Bundle, Battle.net, EA, Amazon Games, Xbox, and local folders) into a centralized location. |
| 6 | + |
| 7 | +**Database Path**: Configured via `DATABASE_PATH` in `config.py` |
| 8 | + |
| 9 | +## Tables |
| 10 | + |
| 11 | +### 1. games |
| 12 | + |
| 13 | +The main table storing all games from all sources. |
| 14 | + |
| 15 | +| Column | Type | Nullable | Description | |
| 16 | +|--------|------|----------|-------------| |
| 17 | +| `id` | INTEGER | No | Primary key, auto-incremented | |
| 18 | +| `name` | TEXT | No | Game title | |
| 19 | +| `store` | TEXT | No | Source store (steam, epic, gog, itch, humble, battlenet, ea, amazon, xbox, local, ubisoft) | |
| 20 | +| `store_id` | TEXT | Yes | Unique identifier from the source store | |
| 21 | +| `description` | TEXT | Yes | Game description/summary | |
| 22 | +| `developers` | TEXT | Yes | JSON array of developer names | |
| 23 | +| `publishers` | TEXT | Yes | JSON array of publisher names | |
| 24 | +| `genres` | TEXT | Yes | JSON array of genre/theme tags | |
| 25 | +| `cover_image` | TEXT | Yes | URL or path to cover/box art image | |
| 26 | +| `background_image` | TEXT | Yes | URL or path to background/hero image | |
| 27 | +| `icon` | TEXT | Yes | URL or path to icon/logo image | |
| 28 | +| `supported_platforms` | TEXT | Yes | JSON array of platform names (Windows, Mac, Linux, Android, etc.) | |
| 29 | +| `release_date` | TEXT | Yes | Release date in ISO format or timestamp | |
| 30 | +| `created_date` | TEXT | Yes | Creation date from store | |
| 31 | +| `last_modified` | TEXT | Yes | Last modification date from store | |
| 32 | +| `playtime_hours` | REAL | Yes | Total hours played (Steam only) | |
| 33 | +| `critics_score` | REAL | Yes | Critic/user score from store (0-100 scale) | |
| 34 | +| `average_rating` | REAL | Yes | Computed average across all available ratings (0-100 scale) | |
| 35 | +| `can_run_offline` | BOOLEAN | Yes | Whether game can run without internet connection | |
| 36 | +| `dlcs` | TEXT | Yes | JSON array of DLC information | |
| 37 | +| `extra_data` | TEXT | Yes | JSON object for store-specific additional data | |
| 38 | +| `added_at` | TIMESTAMP | No | When the game was first added to database (default: current timestamp) | |
| 39 | +| `updated_at` | TIMESTAMP | No | When the game was last updated (default: current timestamp) | |
| 40 | +| `hidden` | BOOLEAN | Yes | User flag to hide game from main views (default: 0) | |
| 41 | +| `nsfw` | BOOLEAN | Yes | User flag to mark game as NSFW (default: 0) | |
| 42 | +| `cover_url_override` | TEXT | Yes | User-specified cover image URL override | |
| 43 | +| `igdb_id` | TEXT | Yes | IGDB identifier for the game | |
| 44 | +| `igdb_rating` | REAL | Yes | IGDB rating (0-100 scale) | |
| 45 | +| `aggregated_rating` | REAL | Yes | IGDB aggregated rating (0-100 scale) | |
| 46 | +| `total_rating` | REAL | Yes | IGDB total rating (0-100 scale) | |
| 47 | +| `metacritic_score` | REAL | Yes | Metacritic critic score (0-100 scale) | |
| 48 | +| `metacritic_user_score` | REAL | Yes | Metacritic user score (0-10 scale) | |
| 49 | +| `metacritic_url` | TEXT | Yes | URL to Metacritic page | |
| 50 | +| `protondb_tier` | TEXT | Yes | ProtonDB compatibility tier (platinum, gold, silver, bronze, borked) | |
| 51 | +| `protondb_score` | REAL | Yes | ProtonDB score (0-100 scale) | |
| 52 | +| `ubisoft_id` | TEXT | Yes | Ubisoft Connect game identifier | |
| 53 | + |
| 54 | +**Indexes:** |
| 55 | +- `idx_games_store` on `store` |
| 56 | +- `idx_games_name` on `name` |
| 57 | + |
| 58 | +**Unique Constraint:** `(store, store_id)` - ensures no duplicate games per store |
| 59 | + |
| 60 | +#### Average Rating Calculation |
| 61 | + |
| 62 | +The `average_rating` column is computed from all available rating sources: |
| 63 | +- `critics_score` (Steam reviews, 0-100) |
| 64 | +- `igdb_rating` (IGDB rating, 0-100) |
| 65 | +- `aggregated_rating` (IGDB aggregated, 0-100) |
| 66 | +- `total_rating` (IGDB total, 0-100) |
| 67 | +- `metacritic_score` (Metacritic critics, 0-100) |
| 68 | +- `metacritic_user_score` (Metacritic users, normalized from 0-10 to 0-100) |
| 69 | + |
| 70 | +All ratings are normalized to a 0-100 scale, then averaged. Returns `None` if no ratings are available. |
| 71 | + |
| 72 | +### 2. collections |
| 73 | + |
| 74 | +User-created game collections for organizing games. |
| 75 | + |
| 76 | +| Column | Type | Nullable | Description | |
| 77 | +|--------|------|----------|-------------| |
| 78 | +| `id` | INTEGER | No | Primary key, auto-incremented | |
| 79 | +| `name` | TEXT | No | Collection name | |
| 80 | +| `description` | TEXT | Yes | Collection description | |
| 81 | +| `created_at` | TIMESTAMP | No | When the collection was created (default: current timestamp) | |
| 82 | +| `updated_at` | TIMESTAMP | No | When the collection was last modified (default: current timestamp) | |
| 83 | + |
| 84 | +### 3. collection_games |
| 85 | + |
| 86 | +Junction table linking games to collections (many-to-many relationship). |
| 87 | + |
| 88 | +| Column | Type | Nullable | Description | |
| 89 | +|--------|------|----------|-------------| |
| 90 | +| `collection_id` | INTEGER | No | Foreign key to collections.id (CASCADE on delete) | |
| 91 | +| `game_id` | INTEGER | No | Foreign key to games.id (CASCADE on delete) | |
| 92 | +| `added_at` | TIMESTAMP | No | When the game was added to collection (default: current timestamp) | |
| 93 | + |
| 94 | +**Primary Key:** `(collection_id, game_id)` |
| 95 | + |
| 96 | +**Foreign Keys:** |
| 97 | +- `collection_id` → `collections(id)` ON DELETE CASCADE |
| 98 | +- `game_id` → `games(id)` ON DELETE CASCADE |
| 99 | + |
| 100 | +### 4. settings |
| 101 | + |
| 102 | +Application settings storage (key-value pairs). |
| 103 | + |
| 104 | +| Column | Type | Nullable | Description | |
| 105 | +|--------|------|----------|-------------| |
| 106 | +| `key` | TEXT | No | Setting key (primary key) | |
| 107 | +| `value` | TEXT | Yes | Setting value (stored as text, JSON for complex values) | |
| 108 | +| `updated_at` | TIMESTAMP | No | When the setting was last updated (default: current timestamp) | |
| 109 | + |
| 110 | +## Store-Specific Data |
| 111 | + |
| 112 | +### Steam |
| 113 | +- `store_id`: Steam AppID |
| 114 | +- `cover_image`: `https://cdn.cloudflare.steamstatic.com/steam/apps/{appid}/library_600x900_2x.jpg` |
| 115 | +- `background_image`: `https://cdn.cloudflare.steamstatic.com/steam/apps/{appid}/library_hero.jpg` |
| 116 | +- `playtime_hours`: Total playtime |
| 117 | +- `critics_score`: User review score (percentage) |
| 118 | + |
| 119 | +### Epic Games Store |
| 120 | +- `store_id`: Epic app_name |
| 121 | +- `can_run_offline`: Offline capability |
| 122 | +- `dlcs`: List of DLCs |
| 123 | + |
| 124 | +### GOG |
| 125 | +- `store_id`: GOG product_id |
| 126 | +- `genres`: Combined genres and themes (deduplicated, case-insensitive) |
| 127 | +- `release_date`: Unix timestamp converted to ISO format |
| 128 | + |
| 129 | +### itch.io |
| 130 | +- `store_id`: itch.io game ID |
| 131 | +- `supported_platforms`: Built from platform flags (windows, mac, linux, android) |
| 132 | + |
| 133 | +### Humble Bundle |
| 134 | +- `store_id`: Humble machine_name |
| 135 | +- `publishers`: Contains payee information |
| 136 | + |
| 137 | +### Battle.net |
| 138 | +- `store_id`: Blizzard title_id |
| 139 | +- `extra_data`: Contains raw Battle.net data |
| 140 | + |
| 141 | +### EA |
| 142 | +- `store_id`: EA offer_id |
| 143 | + |
| 144 | +### Amazon Games |
| 145 | +- `store_id`: Amazon product_id |
| 146 | + |
| 147 | +### Xbox |
| 148 | +- `store_id`: Xbox store ID |
| 149 | +- `extra_data`: Contains: |
| 150 | + - `is_streaming`: Whether it's a cloud streaming game |
| 151 | + - `acquisition_type`: How the game was acquired |
| 152 | + - `title_id`: Xbox title ID |
| 153 | + - `pfn`: Package family name |
| 154 | + |
| 155 | +### Local |
| 156 | +- `store_id`: Generated from folder path |
| 157 | +- `extra_data`: Contains: |
| 158 | + - `folder_path`: Path to game folder |
| 159 | + - `manual_igdb_id`: User-specified IGDB ID for metadata matching |
| 160 | + |
| 161 | +### Ubisoft Connect |
| 162 | +- `store_id`: Ubisoft game ID |
| 163 | +- `ubisoft_id`: Alternative Ubisoft identifier |
| 164 | + |
| 165 | +## Database Connection |
| 166 | + |
| 167 | +The `database.py` module provides: |
| 168 | +- `get_db()`: Returns a connection with `row_factory = sqlite3.Row` for dict-like access |
| 169 | + |
| 170 | +## Migration Functions |
| 171 | + |
| 172 | +The following functions handle database schema migrations: |
| 173 | + |
| 174 | +- `ensure_extra_columns()`: Adds `hidden`, `nsfw`, and `cover_url_override` columns |
| 175 | +- `ensure_collections_tables()`: Creates `collections` and `collection_games` tables |
| 176 | +- `add_average_rating_column()`: Adds `average_rating` column |
| 177 | + |
| 178 | +## Import Pipeline |
| 179 | + |
| 180 | +The `database_builder.py` module contains functions to import games from each store: |
| 181 | + |
| 182 | +1. `create_database()`: Initialize all tables and indexes |
| 183 | +2. `import_steam_games(conn)` |
| 184 | +3. `import_epic_games(conn)` |
| 185 | +4. `import_gog_games(conn)` |
| 186 | +5. `import_itch_games(conn)` |
| 187 | +6. `import_humble_games(conn)` |
| 188 | +7. `import_battlenet_games(conn)` |
| 189 | +8. `import_ea_games(conn)` |
| 190 | +9. `import_amazon_games(conn)` |
| 191 | +10. `import_xbox_games(conn)` |
| 192 | +11. `import_local_games(conn)` |
| 193 | + |
| 194 | +Each import function: |
| 195 | +- Returns the count of imported games |
| 196 | +- Uses `ON CONFLICT(store, store_id) DO UPDATE` to handle duplicates |
| 197 | +- Updates the `updated_at` timestamp |
| 198 | +- Prints progress messages with `[OK]` style indicators |
| 199 | + |
| 200 | +## Utility Functions |
| 201 | + |
| 202 | +### Rating Management |
| 203 | + |
| 204 | +```python |
| 205 | +calculate_average_rating( |
| 206 | + critics_score=None, |
| 207 | + igdb_rating=None, |
| 208 | + aggregated_rating=None, |
| 209 | + total_rating=None, |
| 210 | + metacritic_score=None, |
| 211 | + metacritic_user_score=None |
| 212 | +) -> float | None |
| 213 | +``` |
| 214 | + |
| 215 | +Computes average rating from available sources (0-100 scale). |
| 216 | + |
| 217 | +```python |
| 218 | +update_average_rating(conn, game_id) -> float | None |
| 219 | +``` |
| 220 | + |
| 221 | +Updates the `average_rating` for a specific game by fetching all rating fields and computing the average. |
| 222 | + |
| 223 | +### Statistics |
| 224 | + |
| 225 | +```python |
| 226 | +get_stats(conn) -> dict |
| 227 | +``` |
| 228 | + |
| 229 | +Returns: |
| 230 | +```json |
| 231 | +{ |
| 232 | + "total": 1234, |
| 233 | + "by_store": { |
| 234 | + "steam": 500, |
| 235 | + "epic": 200, |
| 236 | + "gog": 300, |
| 237 | + ... |
| 238 | + } |
| 239 | +} |
| 240 | +``` |
| 241 | + |
| 242 | +## JSON Fields |
| 243 | + |
| 244 | +Several columns store JSON arrays or objects as TEXT: |
| 245 | + |
| 246 | +- `developers`: `["Studio A", "Studio B"]` |
| 247 | +- `publishers`: `["Publisher A"]` |
| 248 | +- `genres`: `["Action", "RPG", "Adventure"]` |
| 249 | +- `supported_platforms`: `["Windows", "Linux"]` |
| 250 | +- `dlcs`: Array of DLC objects |
| 251 | +- `extra_data`: Store-specific additional information |
| 252 | + |
| 253 | +Always use `json.loads()` and `json.dumps()` when reading/writing these fields. |
| 254 | + |
| 255 | +## Best Practices |
| 256 | + |
| 257 | +1. **Always use parameterized queries** to prevent SQL injection |
| 258 | +2. **Commit after batch operations** for performance |
| 259 | +3. **Handle exceptions per-game** during imports to avoid losing entire batch |
| 260 | +4. **Update `updated_at`** whenever modifying game records |
| 261 | +5. **Call `update_average_rating()`** after updating any rating field |
| 262 | +6. **Use `get_db()`** for row factory access to treat rows as dictionaries |
| 263 | +7. **Run migration functions** (`ensure_extra_columns()`, `ensure_collections_tables()`) on startup |
| 264 | + |
| 265 | +## Error Handling |
| 266 | + |
| 267 | +Import functions print errors but continue processing: |
| 268 | +```python |
| 269 | +try: |
| 270 | + # import game |
| 271 | +except Exception as e: |
| 272 | + print(f" Error importing {game.get('name')}: {e}") |
| 273 | +``` |
| 274 | + |
| 275 | +This ensures one failing game doesn't block the entire import process. |
| 276 | + |
| 277 | +## Example Queries |
| 278 | + |
| 279 | +### Get all games from a specific store |
| 280 | +```python |
| 281 | +cursor.execute("SELECT * FROM games WHERE store = ?", ("steam",)) |
| 282 | +``` |
| 283 | + |
| 284 | +### Get games with ratings above 80 |
| 285 | +```python |
| 286 | +cursor.execute("SELECT * FROM games WHERE average_rating >= 80 ORDER BY average_rating DESC") |
| 287 | +``` |
| 288 | + |
| 289 | +### Get games in a collection |
| 290 | +```python |
| 291 | +cursor.execute(""" |
| 292 | + SELECT g.* FROM games g |
| 293 | + JOIN collection_games cg ON g.id = cg.game_id |
| 294 | + WHERE cg.collection_id = ? |
| 295 | +""", (collection_id,)) |
| 296 | +``` |
| 297 | + |
| 298 | +### Search games by name |
| 299 | +```python |
| 300 | +cursor.execute("SELECT * FROM games WHERE name LIKE ? ORDER BY name", (f"%{search_term}%",)) |
| 301 | +``` |
| 302 | + |
| 303 | +### Get hidden/NSFW games |
| 304 | +```python |
| 305 | +cursor.execute("SELECT * FROM games WHERE hidden = 1") |
| 306 | +cursor.execute("SELECT * FROM games WHERE nsfw = 1") |
| 307 | +``` |
0 commit comments