Compare commits
2 Commits
pre_claude
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d837c5501 | |||
| 7e4c194c1f |
@@ -99,15 +99,19 @@ async def inject_metadata(roms: Roms) -> Roms:
|
|||||||
try:
|
try:
|
||||||
await asyncio.sleep(0.25) # keep your throttle
|
await asyncio.sleep(0.25) # keep your throttle
|
||||||
md = await scrape_metadata(game.title, session)
|
md = await scrape_metadata(game.title, session)
|
||||||
except ValueError:
|
results[i] = md
|
||||||
scrape_errors.append(game.title)
|
logging.info(f"Successfully scraped: {game.title} # {i+1}/{len(roms.list)}")
|
||||||
|
except Exception as e:
|
||||||
|
# Handle all exceptions, not just ValueError
|
||||||
|
scrape_errors.append(f"{game.title}: {str(e)}")
|
||||||
md = Metadata(title=game.title, year=extract_year_from_title(game.title))
|
md = Metadata(title=game.title, year=extract_year_from_title(game.title))
|
||||||
# log each item as its done
|
results[i] = md
|
||||||
results[i] = md
|
logging.info(f"Used fallback metadata for: {game.title} # {i+1}/{len(roms.list)}")
|
||||||
logging.info(f"Scraped: {game.title} # {i+1}/{len(roms.list)}")
|
|
||||||
# Log recent errors
|
# Log error details every 5 errors to avoid spam but provide visibility
|
||||||
for err in scrape_errors[-5:]:
|
if len(scrape_errors) % 5 == 0:
|
||||||
logging.warning(f"Scraping error: {err}")
|
logging.warning(f"Scraping error for {game.title}: {str(e)}")
|
||||||
|
logging.info(f"Total scraping errors so far: {len(scrape_errors)}")
|
||||||
|
|
||||||
tasks = [asyncio.create_task(_job(i, game)) for i, game in enumerate(roms.list)]
|
tasks = [asyncio.create_task(_job(i, game)) for i, game in enumerate(roms.list)]
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
@@ -133,27 +137,55 @@ async def filter_new_roms(romlist: Roms, session: Session) -> Roms:
|
|||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
url = f"sqlite+pysqlite:///{config.database_path}"
|
url = f"sqlite+pysqlite:///{config.database_path}"
|
||||||
engine = create_engine(url, future=True)
|
# Use a connection with shorter timeout and WAL mode for better concurrency
|
||||||
|
engine = create_engine(
|
||||||
|
url,
|
||||||
|
future=True,
|
||||||
|
connect_args={
|
||||||
|
"timeout": 10, # 10 second timeout instead of default 30
|
||||||
|
"check_same_thread": False
|
||||||
|
},
|
||||||
|
pool_pre_ping=True
|
||||||
|
)
|
||||||
|
|
||||||
# Database tables are now managed by migrations
|
# Database tables are now managed by migrations
|
||||||
# Base.metadata.create_all(engine)
|
# Base.metadata.create_all(engine)
|
||||||
|
|
||||||
with Session(engine) as s:
|
try:
|
||||||
romlist = await make_romlist()
|
with Session(engine) as s:
|
||||||
new_romlist = await filter_new_roms(romlist, s)
|
# Enable WAL mode for better concurrency
|
||||||
|
try:
|
||||||
if new_romlist.list:
|
s.execute("PRAGMA journal_mode=WAL")
|
||||||
new_romlist = await inject_metadata(new_romlist)
|
s.execute("PRAGMA busy_timeout=5000") # 5 second busy timeout
|
||||||
ingest_roms(new_romlist, s)
|
s.commit()
|
||||||
else:
|
logging.info("Enabled WAL mode for better database concurrency")
|
||||||
logging.info("No new ROMs to scrape!")
|
except Exception as e:
|
||||||
|
logging.warning(f"Could not enable WAL mode: {e}")
|
||||||
|
|
||||||
|
romlist = await make_romlist()
|
||||||
|
new_romlist = await filter_new_roms(romlist, s)
|
||||||
|
|
||||||
|
if new_romlist.list:
|
||||||
|
logging.info(f"Starting metadata scraping for {len(new_romlist.list)} new games")
|
||||||
|
new_romlist = await inject_metadata(new_romlist)
|
||||||
|
|
||||||
|
logging.info("Starting database ingestion with smaller batches")
|
||||||
|
# Use smaller batches to reduce database lock time
|
||||||
|
ingest_roms(new_romlist, s, batch=50)
|
||||||
|
else:
|
||||||
|
logging.info("No new ROMs to scrape!")
|
||||||
|
|
||||||
logging.info("ROM scanning completed")
|
logging.info("ROM scanning completed successfully")
|
||||||
if scrape_errors:
|
if scrape_errors:
|
||||||
logging.warning(f"Total scraping errors: {len(scrape_errors)}")
|
logging.warning(f"Total scraping errors: {len(scrape_errors)}")
|
||||||
for err in scrape_errors:
|
for err in scrape_errors[-10:]: # Show last 10 errors only
|
||||||
logging.warning(f"Failed to scrape: {err}")
|
logging.warning(f"Failed to scrape: {err}")
|
||||||
else:
|
else:
|
||||||
logging.info("ROM scanning completed with no errors")
|
logging.info("ROM scanning completed with no metadata errors")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"ROM scanning failed with error: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Initialize logging
|
# Initialize logging
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ class Config:
|
|||||||
return {
|
return {
|
||||||
"rom_path": str(self.rom_path),
|
"rom_path": str(self.rom_path),
|
||||||
"metadata_path": str(self.metadata_path),
|
"metadata_path": str(self.metadata_path),
|
||||||
|
"database_path": str(self.database_path),
|
||||||
|
"images_path": str(self.images_path),
|
||||||
"host": self.host,
|
"host": self.host,
|
||||||
"port": self.port,
|
"port": self.port,
|
||||||
"websocket_port": self.websocket_port,
|
"websocket_port": self.websocket_port,
|
||||||
@@ -73,21 +75,21 @@ class Config:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
# Ensure config directory exists
|
||||||
if not self.path.parent.exists():
|
if not self.path.parent.exists():
|
||||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
rom_path = input(f"Enter the path to your ROMs [{self.rom_path}] enter for default: ").strip()
|
|
||||||
metadata_path = input(f"Enter the path to your metadata [{self.metadata_path}] enter for default: ").strip()
|
# Create directories if they don't exist
|
||||||
self.rom_path = Path(rom_path) if rom_path else self.rom_path
|
|
||||||
self.metadata_path = Path(metadata_path) if metadata_path else self.metadata_path
|
|
||||||
if not self.rom_path.exists():
|
if not self.rom_path.exists():
|
||||||
self.rom_path.mkdir(parents=True, exist_ok=True)
|
self.rom_path.mkdir(parents=True, exist_ok=True)
|
||||||
if not self.metadata_path.exists():
|
if not self.metadata_path.exists():
|
||||||
self.metadata_path.mkdir(parents=True, exist_ok=True)
|
self.metadata_path.mkdir(parents=True, exist_ok=True)
|
||||||
if not self.images_path.exists():
|
if not self.images_path.exists():
|
||||||
self.images_path.mkdir(parents=True, exist_ok=True)
|
self.images_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Write configuration to file
|
||||||
with open(self.path, 'w') as f:
|
with open(self.path, 'w') as f:
|
||||||
json.dump(self.to_dict(), f, indent=4)
|
json.dump(self.to_dict(), f, indent=4)
|
||||||
f.close()
|
|
||||||
|
|
||||||
def load(self) -> "Config":
|
def load(self) -> "Config":
|
||||||
if self.path.exists():
|
if self.path.exists():
|
||||||
@@ -95,19 +97,26 @@ class Config:
|
|||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
self.rom_path = Path(data.get("rom_path", str(self.rom_path)))
|
self.rom_path = Path(data.get("rom_path", str(self.rom_path)))
|
||||||
self.metadata_path = Path(data.get("metadata_path", str(self.metadata_path)))
|
self.metadata_path = Path(data.get("metadata_path", str(self.metadata_path)))
|
||||||
|
self.database_path = Path(data.get("database_path", str(self.database_path)))
|
||||||
|
self.images_path = Path(data.get("images_path", str(self.images_path)))
|
||||||
self.host = data.get("host", self.host)
|
self.host = data.get("host", self.host)
|
||||||
self.port = data.get("port", self.port)
|
self.port = data.get("port", self.port)
|
||||||
self.websocket_port = data.get("websocket_port", self.websocket_port)
|
self.websocket_port = data.get("websocket_port", self.websocket_port)
|
||||||
|
self.igdb_api_key = data.get("igdb_api_key", self.igdb_api_key)
|
||||||
|
self.igdb_client_id = data.get("igdb_client_id", self.igdb_client_id)
|
||||||
|
|
||||||
|
# Load environment secrets if API keys are still empty
|
||||||
if self.igdb_api_key == "" or self.igdb_client_id == "":
|
if self.igdb_api_key == "" or self.igdb_client_id == "":
|
||||||
secrets = self.load_env_secrets()
|
secrets = self.load_env_secrets()
|
||||||
if secrets:
|
if secrets:
|
||||||
self.igdb_api_key = secrets.get("IGDB_SECRET_KEY", "")
|
self.igdb_api_key = secrets.get("IGDB_SECRET_KEY", self.igdb_api_key)
|
||||||
self.igdb_client_id = secrets.get("IGDB_CLIENT_ID", "")
|
self.igdb_client_id = secrets.get("IGDB_CLIENT_ID", self.igdb_client_id)
|
||||||
f.close()
|
|
||||||
self.save()
|
|
||||||
return self
|
|
||||||
f.close()
|
|
||||||
else:
|
else:
|
||||||
|
# Config file doesn't exist, create it with defaults
|
||||||
|
# Load environment secrets for initial setup
|
||||||
|
secrets = self.load_env_secrets()
|
||||||
|
if secrets:
|
||||||
|
self.igdb_api_key = secrets.get("IGDB_SECRET_KEY", self.igdb_api_key)
|
||||||
|
self.igdb_client_id = secrets.get("IGDB_CLIENT_ID", self.igdb_client_id)
|
||||||
self.save()
|
self.save()
|
||||||
self.load()
|
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -199,43 +199,62 @@ def get_existing_rom_paths(session: Session) -> set[Path]:
|
|||||||
return {game.path.resolve() for game in session.scalars(select(Game_table)).all()}
|
return {game.path.resolve() for game in session.scalars(select(Game_table)).all()}
|
||||||
|
|
||||||
def ingest_roms(roms: Roms, session: Session, *, batch: int = 200) -> int:
|
def ingest_roms(roms: Roms, session: Session, *, batch: int = 200) -> int:
|
||||||
|
import logging
|
||||||
n = 0
|
n = 0
|
||||||
for g in roms.list:
|
for g in roms.list:
|
||||||
game = session.scalar(select(Game_table).where(Game_table.path == g.path))
|
try:
|
||||||
if game is None:
|
game = session.scalar(select(Game_table).where(Game_table.path == g.path))
|
||||||
game = Game_table(title=g.title, path=g.path)
|
if game is None:
|
||||||
session.add(game)
|
game = Game_table(title=g.title, path=g.path)
|
||||||
else:
|
session.add(game)
|
||||||
game.title = g.title
|
logging.info(f"Adding new game: {g.title}")
|
||||||
mdto = g.metadata
|
else:
|
||||||
md = game.metadata_obj
|
game.title = g.title
|
||||||
if md is None:
|
logging.info(f"Updating existing game: {g.title}")
|
||||||
md = Metadata_table(game=game, title=mdto.title or g.title)
|
|
||||||
session.add(md)
|
mdto = g.metadata
|
||||||
|
md = game.metadata_obj
|
||||||
|
if md is None:
|
||||||
|
md = Metadata_table(game=game, title=mdto.title or g.title)
|
||||||
|
session.add(md)
|
||||||
|
|
||||||
md.title = mdto.title or g.title
|
md.title = mdto.title or g.title
|
||||||
md.description = mdto.description
|
md.description = mdto.description
|
||||||
md.year = mdto.year if mdto.year is not None else extract_year_from_title(md.title)
|
md.year = mdto.year if mdto.year is not None else extract_year_from_title(md.title)
|
||||||
md.developer = mdto.developer
|
md.developer = mdto.developer
|
||||||
md.publisher = mdto.publisher
|
md.publisher = mdto.publisher
|
||||||
md.players = mdto.players
|
md.players = mdto.players
|
||||||
md.cover_image = mdto.cover_image
|
md.cover_image = mdto.cover_image
|
||||||
md.screenshot = mdto.screenshot
|
md.screenshot = mdto.screenshot
|
||||||
md.cover_image_path = mdto.cover_image_path
|
md.cover_image_path = mdto.cover_image_path
|
||||||
md.screenshot_path = mdto.screenshot_path
|
md.screenshot_path = mdto.screenshot_path
|
||||||
|
|
||||||
try: genres = sorted({s.strip() for s in (mdto.genre or []) if s and s.strip()})
|
try: genres = sorted({s.strip() for s in (mdto.genre or []) if s and s.strip()})
|
||||||
except: genres = []
|
except: genres = []
|
||||||
try: tags = sorted({s.strip() for s in (mdto.tags or []) if s and s.strip()})
|
try: tags = sorted({s.strip() for s in (mdto.tags or []) if s and s.strip()})
|
||||||
except: tags = []
|
except: tags = []
|
||||||
|
|
||||||
md.genre = [_get_or_create_by_name(session, Genre_table, name) for name in genres]
|
md.genre = [_get_or_create_by_name(session, Genre_table, name) for name in genres]
|
||||||
md.tags = [_get_or_create_by_name(session, Tags_table, name) for name in tags]
|
md.tags = [_get_or_create_by_name(session, Tags_table, name) for name in tags]
|
||||||
|
|
||||||
n += 1
|
n += 1
|
||||||
if n % batch == 0:
|
|
||||||
session.flush()
|
# Use more frequent flushes and commits to reduce lock time
|
||||||
|
if n % batch == 0:
|
||||||
|
session.commit() # Commit more frequently to reduce lock duration
|
||||||
|
logging.info(f"Committed batch of {batch} games to database ({n} total)")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to ingest game {g.title}: {e}")
|
||||||
|
session.rollback() # Rollback on error to prevent corruption
|
||||||
|
continue
|
||||||
|
|
||||||
session.commit()
|
# Final commit for remaining items
|
||||||
|
try:
|
||||||
|
session.commit()
|
||||||
|
logging.info(f"Successfully ingested {n} games to database")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed final commit during ROM ingestion: {e}")
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ from typing import Optional, Annotated
|
|||||||
from datetime import timedelta, datetime, timezone
|
from datetime import timedelta, datetime, timezone
|
||||||
import re
|
import re
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, Depends, HTTPException, status, Request, Form, Query, BackgroundTasks
|
from fastapi import FastAPI, Depends, HTTPException, status, Request, Form, Query, BackgroundTasks
|
||||||
@@ -15,18 +17,16 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.exception_handlers import http_exception_handler
|
from fastapi.exception_handlers import http_exception_handler
|
||||||
from fastapi.exceptions import HTTPException as StarletteHTTPException
|
from fastapi.exceptions import HTTPException as StarletteHTTPException
|
||||||
from starlette.exceptions import HTTPException as StarletteBaseHTTPException
|
|
||||||
from sqlalchemy import create_engine, select, func
|
from sqlalchemy import create_engine, select, func
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
from alembic.config import Config as AlembicConfig
|
from alembic.config import Config as AlembicConfig
|
||||||
from alembic import command
|
from alembic import command
|
||||||
import subprocess
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try relative imports first (when run as module)
|
# Try relative imports first (when run as module)
|
||||||
from .libs.config import Config
|
from .libs.config import Config
|
||||||
from .libs.database import Base, Game_table, Metadata_table, User_table, UserRole, user_favorites, Tags_table, Genre_table
|
from .libs.database import Game_table, Metadata_table, User_table, UserRole, user_favorites, Tags_table, Genre_table
|
||||||
from .libs.auth import AuthManager, ACCESS_TOKEN_EXPIRE_MINUTES
|
from .libs.auth import AuthManager, ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
from .libs.logging import get_log_manager
|
from .libs.logging import get_log_manager
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -36,13 +36,31 @@ except ImportError:
|
|||||||
from libs.auth import AuthManager, ACCESS_TOKEN_EXPIRE_MINUTES
|
from libs.auth import AuthManager, ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
from libs.logging import get_log_manager
|
from libs.logging import get_log_manager
|
||||||
|
|
||||||
|
# Initialize logging system first
|
||||||
|
get_log_manager()
|
||||||
|
|
||||||
config = Config()
|
config = Config()
|
||||||
engine = create_engine(f"sqlite+pysqlite:///{config.database_path}", echo=False)
|
engine = create_engine(
|
||||||
|
f"sqlite+pysqlite:///{config.database_path}",
|
||||||
|
echo=False,
|
||||||
|
connect_args={
|
||||||
|
"timeout": 10, # 10 second timeout
|
||||||
|
"check_same_thread": False
|
||||||
|
},
|
||||||
|
pool_pre_ping=True
|
||||||
|
)
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
# Initialize logging system
|
# Enable WAL mode for better concurrency during startup
|
||||||
import logging
|
try:
|
||||||
get_log_manager()
|
with engine.connect() as conn:
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
conn.execute("PRAGMA busy_timeout=5000") # 5 second busy timeout
|
||||||
|
conn.commit()
|
||||||
|
logging.info("Enabled WAL mode for web application database")
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Could not enable WAL mode for web application: {e}")
|
||||||
|
|
||||||
logging.info("DosVault web application starting up")
|
logging.info("DosVault web application starting up")
|
||||||
|
|
||||||
app = FastAPI(title="DOS Frontend", description="ROM Management System")
|
app = FastAPI(title="DOS Frontend", description="ROM Management System")
|
||||||
@@ -119,7 +137,7 @@ def ensure_super_user():
|
|||||||
# Create default super user
|
# Create default super user
|
||||||
logging.info("No super user found, creating default admin user...")
|
logging.info("No super user found, creating default admin user...")
|
||||||
try:
|
try:
|
||||||
default_admin = AuthManager.create_user(
|
AuthManager.create_user(
|
||||||
session=db,
|
session=db,
|
||||||
username="admin",
|
username="admin",
|
||||||
email="admin@dosvault.local",
|
email="admin@dosvault.local",
|
||||||
@@ -807,7 +825,7 @@ async def create_user(
|
|||||||
if existing_user:
|
if existing_user:
|
||||||
raise HTTPException(status_code=400, detail="Username or email already exists")
|
raise HTTPException(status_code=400, detail="Username or email already exists")
|
||||||
|
|
||||||
new_user = AuthManager.create_user(db, username, email, password, role)
|
AuthManager.create_user(db, username, email, password, role)
|
||||||
return RedirectResponse(url="/admin/users", status_code=303)
|
return RedirectResponse(url="/admin/users", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@@ -1017,6 +1035,21 @@ async def admin_rom_scan(
|
|||||||
|
|
||||||
return {"status": "started", "message": "ROM scan started"}
|
return {"status": "started", "message": "ROM scan started"}
|
||||||
|
|
||||||
|
@app.post("/api/admin/game-scan")
|
||||||
|
async def admin_game_scan(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user: User_table = Depends(require_super_user)
|
||||||
|
):
|
||||||
|
"""Trigger game scan in the background (alias for ROM scan)"""
|
||||||
|
if "game_scan" in running_tasks and not running_tasks["game_scan"].done():
|
||||||
|
return {"status": "already_running", "message": "Game scan is already in progress"}
|
||||||
|
|
||||||
|
task = asyncio.create_task(run_rom_scan())
|
||||||
|
running_tasks["game_scan"] = task
|
||||||
|
|
||||||
|
return {"status": "started", "message": "Game scan started"}
|
||||||
|
|
||||||
@app.post("/api/admin/metadata-refresh")
|
@app.post("/api/admin/metadata-refresh")
|
||||||
async def admin_metadata_refresh(
|
async def admin_metadata_refresh(
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
@@ -1134,7 +1167,7 @@ async def admin_system_stats(
|
|||||||
"disk_usage": disk_usage,
|
"disk_usage": disk_usage,
|
||||||
"running_tasks": {
|
"running_tasks": {
|
||||||
task_name: not running_tasks[task_name].done() if task_name in running_tasks else False
|
task_name: not running_tasks[task_name].done() if task_name in running_tasks else False
|
||||||
for task_name in ["rom_scan", "metadata_refresh", "image_sync"]
|
for task_name in ["rom_scan", "game_scan", "metadata_refresh", "image_sync"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1142,31 +1175,43 @@ async def run_rom_scan():
|
|||||||
"""Run the ROM scanner subprocess"""
|
"""Run the ROM scanner subprocess"""
|
||||||
try:
|
try:
|
||||||
logging.info("Starting ROM scan subprocess")
|
logging.info("Starting ROM scan subprocess")
|
||||||
|
# Use the same approach as devenv - run the script directly with proper PYTHONPATH
|
||||||
|
script_path = Path(__file__).parent / "__main__.py" # Point to src/__main__.py
|
||||||
|
|
||||||
|
# Create subprocess with real-time output capture
|
||||||
process = await asyncio.create_subprocess_exec(
|
process = await asyncio.create_subprocess_exec(
|
||||||
"python", "-m", "src",
|
"python", str(script_path),
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE
|
stderr=asyncio.subprocess.STDOUT, # Merge stderr into stdout for unified logging
|
||||||
|
cwd=Path(__file__).parent.parent # Set working directory to project root
|
||||||
)
|
)
|
||||||
stdout, stderr = await process.communicate()
|
|
||||||
|
|
||||||
# Log the output for visibility
|
# Capture output in real-time and log it
|
||||||
if stdout:
|
output_lines = []
|
||||||
for line in stdout.decode().strip().split('\n'):
|
try:
|
||||||
if line.strip():
|
while True:
|
||||||
logging.info(f"ROM Scanner: {line.strip()}")
|
line = await process.stdout.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
|
decoded_line = line.decode().rstrip()
|
||||||
|
if decoded_line:
|
||||||
|
output_lines.append(decoded_line)
|
||||||
|
# Log to main application log immediately for real-time visibility
|
||||||
|
logging.info(f"ROM Scanner: {decoded_line}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error reading ROM scanner output: {e}")
|
||||||
|
|
||||||
if stderr:
|
# Wait for process to complete
|
||||||
for line in stderr.decode().strip().split('\n'):
|
await process.wait()
|
||||||
if line.strip():
|
|
||||||
logging.error(f"ROM Scanner Error: {line.strip()}")
|
|
||||||
|
|
||||||
success = process.returncode == 0
|
success = process.returncode == 0
|
||||||
logging.info(f"ROM scan subprocess completed with exit code: {process.returncode}")
|
logging.info(f"ROM scan subprocess completed with exit code: {process.returncode}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": success,
|
"success": success,
|
||||||
"output": stdout.decode(),
|
"output": '\n'.join(output_lines),
|
||||||
"error": stderr.decode(),
|
"error": "",
|
||||||
"returncode": process.returncode
|
"returncode": process.returncode
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1305,22 +1305,22 @@ async function loadConfiguration() {
|
|||||||
|
|
||||||
function populateConfigForm(config) {
|
function populateConfigForm(config) {
|
||||||
// Populate form fields
|
// Populate form fields
|
||||||
document.getElementById('config-host').value = config.host || '';
|
document.getElementById('config-host').value = config.config?.host || config.host || '';
|
||||||
document.getElementById('config-port').value = config.port || '';
|
document.getElementById('config-port').value = config.config?.port || config.port || '';
|
||||||
document.getElementById('config-rom-path').value = config.game_path || '';
|
document.getElementById('config-rom-path').value = config.config?.rom_path || config.rom_path || '';
|
||||||
document.getElementById('config-images-path').value = config.images_path || '';
|
document.getElementById('config-images-path').value = config.config?.images_path || config.images_path || '';
|
||||||
document.getElementById('config-igdb-client-id').value = config.igdb_client_id || '';
|
document.getElementById('config-igdb-client-id').value = config.config?.igdb_client_id || config.igdb_client_id || '';
|
||||||
document.getElementById('config-igdb-secret').value = config.igdb_api_key || '';
|
document.getElementById('config-igdb-secret').value = config.config?.igdb_api_key || config.igdb_api_key || '';
|
||||||
|
|
||||||
// Populate JSON editor
|
// Populate JSON editor
|
||||||
document.getElementById('config-json').value = JSON.stringify(config, null, 2);
|
document.getElementById('config-json').value = JSON.stringify(config.config || config, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectConfigFromForm() {
|
function collectConfigFromForm() {
|
||||||
const config = {
|
const config = {
|
||||||
host: document.getElementById('config-host').value,
|
host: document.getElementById('config-host').value,
|
||||||
port: parseInt(document.getElementById('config-port').value) || 8080,
|
port: parseInt(document.getElementById('config-port').value) || 8080,
|
||||||
game_path: document.getElementById('config-rom-path').value,
|
rom_path: document.getElementById('config-rom-path').value,
|
||||||
images_path: document.getElementById('config-images-path').value,
|
images_path: document.getElementById('config-images-path').value,
|
||||||
igdb_client_id: document.getElementById('config-igdb-client-id').value,
|
igdb_client_id: document.getElementById('config-igdb-client-id').value,
|
||||||
igdb_api_key: document.getElementById('config-igdb-secret').value
|
igdb_api_key: document.getElementById('config-igdb-secret').value
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
Test script to download images for existing games that don't have local images yet.
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
from sqlalchemy import create_engine, select
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from src.libs.config import Config
|
|
||||||
from src.libs.database import Game_table, Metadata_table
|
|
||||||
from src.libs.functions import download_image, get_image_filename
|
|
||||||
|
|
||||||
async def test_image_downloads():
|
|
||||||
config = Config()
|
|
||||||
url = f"sqlite+pysqlite:///{config.database_path}"
|
|
||||||
engine = create_engine(url, future=True)
|
|
||||||
|
|
||||||
with Session(engine) as session:
|
|
||||||
# Get first 3 games that have remote images but no local images
|
|
||||||
stmt = (
|
|
||||||
select(Game_table)
|
|
||||||
.join(Metadata_table)
|
|
||||||
.where(
|
|
||||||
(Metadata_table.cover_image.is_not(None)) &
|
|
||||||
(Metadata_table.cover_image_path.is_(None))
|
|
||||||
)
|
|
||||||
.limit(3)
|
|
||||||
)
|
|
||||||
games = session.scalars(stmt).all()
|
|
||||||
|
|
||||||
print(f"Found {len(games)} games to test image downloads for")
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as http_session:
|
|
||||||
for game in games:
|
|
||||||
metadata = game.metadata_obj
|
|
||||||
print(f"\nTesting: {game.title}")
|
|
||||||
|
|
||||||
# Download cover image
|
|
||||||
if metadata.cover_image:
|
|
||||||
cover_filename = get_image_filename(metadata.cover_image, game.title, 'cover')
|
|
||||||
cover_path = config.images_path / cover_filename
|
|
||||||
|
|
||||||
print(f" Downloading cover: {metadata.cover_image}")
|
|
||||||
success = await download_image(metadata.cover_image, cover_path, http_session)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
print(f" ✓ Cover saved to: {cover_path}")
|
|
||||||
# Update database with local path
|
|
||||||
metadata.cover_image_path = cover_path
|
|
||||||
else:
|
|
||||||
print(" ✗ Failed to download cover")
|
|
||||||
|
|
||||||
# Download screenshot
|
|
||||||
if metadata.screenshot:
|
|
||||||
screenshot_filename = get_image_filename(metadata.screenshot, game.title, 'screenshot')
|
|
||||||
screenshot_path = config.images_path / screenshot_filename
|
|
||||||
|
|
||||||
print(f" Downloading screenshot: {metadata.screenshot}")
|
|
||||||
success = await download_image(metadata.screenshot, screenshot_path, http_session)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
print(f" ✓ Screenshot saved to: {screenshot_path}")
|
|
||||||
# Update database with local path
|
|
||||||
metadata.screenshot_path = screenshot_path
|
|
||||||
else:
|
|
||||||
print(" ✗ Failed to download screenshot")
|
|
||||||
|
|
||||||
# Commit the updates
|
|
||||||
session.commit()
|
|
||||||
print("\n✓ Database updated with local image paths")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(test_image_downloads())
|
|
||||||
Reference in New Issue
Block a user