Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Notable changes to UnityScraper are documented here. The project follows

### Added

- Offline XboxUnity title catalog with background refresh, sync history,
TitleID/name autocomplete, and a manual/CLI refresh path.
- Additive schema migration 5 for cached XboxUnity titles and catalog sync
runs.
- Versioned additive migrations for collection snapshots, preservation
matches, repair plans, console inventories, resumable jobs, overrides, and
recovery state.
Expand Down Expand Up @@ -35,6 +39,11 @@ Notable changes to UnityScraper are documented here. The project follows

### Changed

- Library rows now show `Unknown game` instead of duplicating the TitleID when
no real game name is known.
- Cached XboxUnity names enrich only blank, unknown, or TitleID-shaped values
and never replace an existing preferred title.
- Library queries now close SQLite handles immediately after use.
- Version advanced to `1.0.0-beta.1`.
- Download queues now use atomic writes and recover interrupted items.
- Update checks select a platform artifact and require its SHA-256 sidecar
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ archives before running large imports or transfers.
### Library and Downloads

- Collects XboxUnity cover and Title Update metadata.
- Caches the XboxUnity title catalog for offline name and TitleID autocomplete.
- Reviews results before selectively downloading files.
- Tracks pending, downloaded, failed, and verified content.
- Supports retries, rate limiting, bandwidth limits, and resumable downloads.
Expand Down Expand Up @@ -128,7 +129,7 @@ Linux source setup:
| Workspace | Purpose |
| --- | --- |
| Library | Browse games, covers, MediaIDs, and available updates |
| Add Games | Import or enter TitleIDs |
| Add Games | Search cached game names, select TitleIDs, or import lists |
| Downloads | Review and manage download activity |
| Backup Manager | Scan, install, verify, export, convert, and transfer owned content |
| Collections | Identify storage, compare Title Updates, verify preservation data, and preview repairs |
Expand Down Expand Up @@ -173,6 +174,9 @@ python main.py --help
### XboxUnity Metadata

```powershell
# Refresh every XboxUnity title name for local autocomplete
python main.py --sync-title-catalog

# Collect metadata for one or more TitleIDs
python main.py 4D5307E6 --metadata-only

Expand All @@ -186,6 +190,15 @@ python main.py --verify-integrity
Providing TitleIDs without `--metadata-only` starts the download workflow.
Review the destination and settings before doing this.

The desktop application refreshes the XboxUnity title catalog in the
background when the local copy is missing or more than seven days old. The
**Add Games** search box always queries SQLite, so suggestions remain fast and
available offline. Suggestions include the game name, TitleID, and content
type. Use **Refresh Catalog** on that page to request an immediate update.

Catalog names fill only blank, unknown, or accidentally TitleID-shaped game
names. Existing user names and better source-attributed metadata are preserved.

### Knowledge Sources

```powershell
Expand Down
38 changes: 38 additions & 0 deletions database.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def add_titleid(self, titleid: str, name: Optional[str] = None,
# Update search index
self._update_search_index(conn, titleid, name, publisher, metadata)
self._enrich_unknown_titleid_metadata(conn, titleid)
self._enrich_unknown_titleid_from_catalog(conn, titleid)

logger.info(f"Added/updated TitleID: {titleid}")
return True
Expand Down Expand Up @@ -243,6 +244,43 @@ def _enrich_unknown_titleid_metadata(self, conn, titleid: str) -> int:
)
self._update_search_index(conn, titleid, new_name, new_publisher, metadata)
return 1

def _enrich_unknown_titleid_from_catalog(self, conn, titleid: str) -> int:
"""Use the cached XboxUnity title only when the library name is unknown."""
row = conn.execute(
"""
SELECT t.name, t.publisher, t.metadata, c.name AS catalog_name
FROM titleids AS t
LEFT JOIN xboxunity_title_catalog AS c ON c.titleid = t.titleid
WHERE t.titleid = ?
""",
(titleid,),
).fetchone()
if not row or not row["catalog_name"]:
return 0
current_name = row["name"]
if not (is_unknown(current_name) or str(current_name).upper() == titleid.upper()):
return 0

metadata = {}
if row["metadata"]:
try:
metadata = json.loads(row["metadata"])
except json.JSONDecodeError:
metadata = {}
metadata["title_source"] = "XboxUnity title catalog"
conn.execute(
"UPDATE titleids SET name = ?, metadata = ? WHERE titleid = ?",
(row["catalog_name"], json.dumps(metadata, sort_keys=True), titleid),
)
self._update_search_index(
conn,
titleid,
row["catalog_name"],
row["publisher"],
metadata,
)
return 1

def _update_search_index(self, conn, titleid: str, name: Optional[str] = None,
publisher: Optional[str] = None, metadata: Optional[Dict] = None):
Expand Down
40 changes: 39 additions & 1 deletion database_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pathlib import Path


SCHEMA_VERSION = 4
SCHEMA_VERSION = 5


def _now() -> str:
Expand Down Expand Up @@ -65,6 +65,7 @@ def ensure_application_schema(connection: sqlite3.Connection) -> int:
(2, "preservation records", _migration_preservation),
(3, "console synchronization", _migration_console_sync),
(4, "user overrides and recovery", _migration_reliability),
(5, "XboxUnity title catalog", _migration_xboxunity_catalog),
)
for version, name, migration in migrations:
if version in applied:
Expand Down Expand Up @@ -240,3 +241,40 @@ def _migration_reliability(connection: sqlite3.Connection) -> None:
);
"""
)


def _migration_xboxunity_catalog(connection: sqlite3.Connection) -> None:
connection.executescript(
"""
CREATE TABLE IF NOT EXISTS xboxunity_title_catalog (
titleid TEXT PRIMARY KEY,
name TEXT NOT NULL,
hb_titleid TEXT,
title_type TEXT,
link_enabled INTEGER NOT NULL DEFAULT 0,
covers_count INTEGER NOT NULL DEFAULT 0,
updates_count INTEGER NOT NULL DEFAULT 0,
media_id_count INTEGER NOT NULL DEFAULT 0,
user_count INTEGER NOT NULL DEFAULT 0,
newest_content TEXT,
source_url TEXT NOT NULL,
raw_json TEXT NOT NULL,
fetched_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_xboxunity_catalog_name
ON xboxunity_title_catalog(name COLLATE NOCASE);
CREATE INDEX IF NOT EXISTS idx_xboxunity_catalog_type
ON xboxunity_title_catalog(title_type);

CREATE TABLE IF NOT EXISTS xboxunity_catalog_sync_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
started_at TEXT NOT NULL,
completed_at TEXT,
status TEXT NOT NULL,
pages_expected INTEGER NOT NULL DEFAULT 0,
pages_fetched INTEGER NOT NULL DEFAULT 0,
items_upserted INTEGER NOT NULL DEFAULT 0,
error_message TEXT
);
"""
)
18 changes: 12 additions & 6 deletions library_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import hashlib
import json
import sqlite3
from contextlib import closing
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable, Optional
Expand Down Expand Up @@ -52,7 +53,12 @@ def list_games(self, search: str = "") -> list[GameSummary]:
query = """
SELECT
t.titleid,
COALESCE(NULLIF(t.name, ''), t.titleid) AS name,
CASE
WHEN t.name IS NULL OR TRIM(t.name) = ''
OR UPPER(TRIM(t.name)) = UPPER(t.titleid)
THEN 'Unknown game'
ELSE t.name
END AS name,
COALESCE(t.publisher, '') AS publisher,
COALESCE(t.last_scraped, '') AS last_scraped,
COUNT(DISTINCT c.id) AS covers_total,
Expand Down Expand Up @@ -83,7 +89,7 @@ def list_games(self, search: str = "") -> list[GameSummary]:
ORDER BY name COLLATE NOCASE, t.titleid
"""

with self._connect() as connection:
with closing(self._connect()) as connection:
rows = connection.execute(query, parameters).fetchall()

return [
Expand All @@ -106,7 +112,7 @@ def get_game_details(self, titleid: str) -> dict[str, Any]:
if not self.database_path.exists():
return {}

with self._connect() as connection:
with closing(self._connect()) as connection:
title = connection.execute(
"SELECT * FROM titleids WHERE titleid = ?",
(titleid,),
Expand Down Expand Up @@ -174,7 +180,7 @@ def get_dashboard_counts(self) -> dict[str, int]:
),
}

with self._connect() as connection:
with closing(self._connect()) as connection:
return {
name: int(connection.execute(statement).fetchone()[0] or 0)
for name, statement in sql.items()
Expand All @@ -191,7 +197,7 @@ def find_database_duplicates(self) -> list[dict[str, Any]]:
if not self.database_path.exists():
return []

with self._connect() as connection:
with closing(self._connect()) as connection:
update_rows = connection.execute(
"""
SELECT
Expand Down Expand Up @@ -236,7 +242,7 @@ def scan_archive_health(self) -> dict[str, Any]:
if not self.database_path.exists():
return report

with self._connect() as connection:
with closing(self._connect()) as connection:
rows = connection.execute(
"""
SELECT 'cover' AS item_type, id, titleid, file_path, file_size
Expand Down
27 changes: 27 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,11 @@ def main():
'environment variable'
)
)
parser.add_argument(
'--sync-title-catalog',
action='store_true',
help='Refresh the local XboxUnity title-name catalog for offline autocomplete'
)
parser.add_argument(
'--sync-knowledge',
action='store_true',
Expand Down Expand Up @@ -948,6 +953,28 @@ def main():
logger.info("Configuration saved")

# Initialize scraper
if args.sync_title_catalog:
try:
from title_catalog import XboxUnityTitleCatalog

DatabaseManager()
summary = XboxUnityTitleCatalog(
request_interval=config.rate_limit,
timeout=config.timeout,
).sync(
progress=lambda page, pages, items: logger.info(
"XboxUnity catalog page %s/%s (%s titles)",
page,
pages,
items,
)
)
logger.info("XboxUnity title catalog sync completed: %s", summary)
sys.exit(0)
except Exception as e:
logger.error("XboxUnity title catalog sync failed: %s", e)
sys.exit(1)

if args.sync_knowledge:
try:
from knowledge_sync import sync_consolemods_knowledge
Expand Down
Loading
Loading