diff --git a/devenv.nix b/devenv.nix
index d947692..c5fe070 100644
--- a/devenv.nix
+++ b/devenv.nix
@@ -42,6 +42,7 @@
passlib
alembic
aiohttp
+ websockets
'';
};
# uv = {
diff --git a/src/__main__.py b/src/__main__.py
index a71cd8e..16147da 100755
--- a/src/__main__.py
+++ b/src/__main__.py
@@ -44,7 +44,7 @@ async def scrape_metadata(title: str, session: aiohttp.ClientSession) -> Metadat
cover_data = game_data.get('cover')
if cover_data and cover_data.get('image_id'):
md.cover_image = IGDB.build_cover_url(cover_data['image_id'], 'cover_big')
-
+
# Download cover image locally
cover_filename = get_image_filename(md.cover_image, title, 'cover')
cover_path = config.images_path / cover_filename
@@ -60,7 +60,7 @@ async def scrape_metadata(title: str, session: aiohttp.ClientSession) -> Metadat
artworks = game_data.get('artworks', [])
if artworks and artworks[0].get('image_id'):
md.screenshot = IGDB.build_cover_url(artworks[0]['image_id'], 'screenshot_med')
-
+
# Download screenshot locally
screenshot_filename = get_image_filename(md.screenshot, title, 'screenshot')
screenshot_path = config.images_path / screenshot_filename
@@ -107,7 +107,7 @@ async def inject_metadata(roms: Roms) -> Roms:
md = Metadata(title=game.title, year=extract_year_from_title(game.title))
results[i] = md
logging.info(f"Used fallback metadata for: {game.title} # {i+1}/{len(roms.list)}")
-
+
# Log error details every 5 errors to avoid spam but provide visibility
if len(scrape_errors) % 5 == 0:
logging.warning(f"Scraping error for {game.title}: {str(e)}")
@@ -124,20 +124,19 @@ async def inject_metadata(roms: Roms) -> Roms:
async def filter_new_roms(romlist: Roms, session: Session) -> Roms:
existing_paths = get_existing_rom_paths(session)
new_roms = Roms()
-
+
for game in romlist.list:
if game.path.resolve() not in existing_paths:
new_roms.list.append(game)
-
+
logging.info(f"Found {len(romlist.list)} total ROMs")
logging.info(f"Found {len(existing_paths)} existing ROMs in database")
logging.info(f"Will scrape {len(new_roms.list)} new ROMs")
-
+
return new_roms
async def main():
url = f"sqlite+pysqlite:///{config.database_path}"
- # Use a connection with shorter timeout and WAL mode for better concurrency
engine = create_engine(
url,
future=True,
@@ -147,10 +146,7 @@ async def main():
},
pool_pre_ping=True
)
-
- # Database tables are now managed by migrations
- # Base.metadata.create_all(engine)
-
+
try:
with Session(engine) as s:
# Enable WAL mode for better concurrency
@@ -161,14 +157,14 @@ async def main():
logging.info("Enabled WAL mode for better database concurrency")
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)
@@ -182,7 +178,7 @@ async def main():
logging.warning(f"Failed to scrape: {err}")
else:
logging.info("ROM scanning completed with no metadata errors")
-
+
except Exception as e:
logging.error(f"ROM scanning failed with error: {e}")
raise
diff --git a/src/libs/auth.py b/src/libs/auth.py
index d1a7a17..f8412ca 100644
--- a/src/libs/auth.py
+++ b/src/libs/auth.py
@@ -7,8 +7,9 @@ from jose import JWTError, jwt
from sqlalchemy.orm import Session
from sqlalchemy import select
from .database import User_table, UserRole
+from .config import Config
-SECRET_KEY = "your-secret-key-change-this-in-production"
+SECRET_KEY = Config().secret_key
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
@@ -71,4 +72,4 @@ class AuthManager:
session.add(user)
session.commit()
session.refresh(user)
- return user
\ No newline at end of file
+ return user
diff --git a/src/libs/config.py b/src/libs/config.py
index 940afa7..9e45e17 100644
--- a/src/libs/config.py
+++ b/src/libs/config.py
@@ -27,6 +27,7 @@ class Config:
websocket_port: int = 8081
igdb_api_key: str = ""
igdb_client_id: str = ""
+ secret_key: str = ""
def __init__(self, path: Optional[Path] = None):
if path:
@@ -74,6 +75,12 @@ class Config:
"igdb_client_id": self.igdb_client_id,
}
+ def make_secret_key(self, length: int = 32) -> str:
+ import secrets
+ import string
+ alphabet = string.ascii_letters + string.digits
+ return ''.join(secrets.choice(alphabet) for _ in range(length))
+
def save(self):
# Ensure config directory exists
if not self.path.parent.exists():
@@ -104,6 +111,7 @@ class Config:
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)
+ self.secret_key = data.get("secret_key", self.secret_key)
# Load environment secrets if API keys are still empty
if self.igdb_api_key == "" or self.igdb_client_id == "":
@@ -118,5 +126,6 @@ class Config:
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.secret_key = self.make_secret_key(32)
self.save()
return self
diff --git a/src/webapp.py b/src/webapp.py
index d2ca34a..c82cee5 100755
--- a/src/webapp.py
+++ b/src/webapp.py
@@ -9,7 +9,7 @@ import logging
import subprocess
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, WebSocket
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse, JSONResponse
from fastapi.templating import Jinja2Templates
@@ -22,6 +22,7 @@ from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.exc import OperationalError
from alembic.config import Config as AlembicConfig
from alembic import command
+from websockets.asyncio.client import connect
try:
# Try relative imports first (when run as module)
@@ -626,6 +627,50 @@ async def toggle_favorite(
db.commit()
return {"message": f"Game {action} from favorites"}
+@app.post("/run/{game_id}")
+async def run_game(request: Request, game_id: int, db: Session = Depends(get_db), current_user: User_table = Depends(require_auth)):
+ logging.info(f"Run request for game ID {game_id} from user {current_user.username}")
+ if current_user.role == UserRole.DEMO.value:
+ raise HTTPException(status_code=403, detail="Demo users cannot download games")
+
+ game = db.get(Game_table, game_id)
+ if not game:
+ raise HTTPException(status_code=404, detail="Game not found")
+
+ if not game.path.exists():
+ raise HTTPException(status_code=404, detail="Game file not found")
+ forwarded_for = request.headers.get("x-forwarded-for")
+ if forwarded_for:
+ ip_address = forwarded_for.split(",")[0].strip()
+ elif real_ip := request.headers.get("x-real-ip"):
+ ip_address = real_ip.strip()
+ elif request.client:
+ ip_address = request.client.host
+ else:
+ ip_address = "unknown"
+ logging.warning("Could not determine client IP address")
+ return HTTPException(status_code=400, detail="Could not determine client IP address")
+ web_socket_port: int = config.websocket_port
+ breakpoint()
+ try:
+ ws = connect(f"ws://{ip_address}:{web_socket_port}")
+ except Exception as e:
+ logging.error(f"WebSocket connection error: {e}")
+ raise HTTPException(status_code=400, detail="Could not connect to client WebSocket")
+ try:
+ async with ws as websocket:
+ await websocket.send(f"RUN::{game.path}")
+ response = await websocket.recv()
+ if response != "OK":
+ logging.error(f"Client returned error: {response}")
+ raise HTTPException(status_code=400, detail=f"Client error: {response}")
+ else:
+ logging.info(f"Sent RUN command for game {game.title} to {ip_address}")
+ except Exception as e:
+ logging.error(f"WebSocket communication error: {e}")
+ raise HTTPException(status_code=400, detail="Error communicating with client WebSocket")
+ return {"web_socket_port": web_socket_port, "game_path": str(game.path)}
+
@app.get("/download/{game_id}")
async def download_game(
@@ -1639,4 +1684,4 @@ async def catch_all_404(request: Request, path: str):
if __name__ == "__main__":
import uvicorn
- uvicorn.run(app, host=config.host, port=config.port)
\ No newline at end of file
+ uvicorn.run(app, host=config.host, port=config.port)
diff --git a/templates/game_detail.html b/templates/game_detail.html
index e167622..66b8688 100644
--- a/templates/game_detail.html
+++ b/templates/game_detail.html
@@ -38,6 +38,10 @@
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded">
Download Game
+
{% else %}
{% if current_user %}Demo Mode - No Downloads{% else %}Login to Download{% endif %}
@@ -287,5 +291,34 @@
alert('Download failed. Please try again.');
}
}
+
+ async function runGame(gameId) {
+ const token = localStorage.getItem('authToken');
+ if (!token) {
+ showLogin();
+ return;
+ }
+
+ try {
+ const response = await fetch(`/run/${gameId}`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (response.ok) {
+ alert('Game is starting in your DOSBox environment.');
+ } else if (response.status === 401) {
+ localStorage.removeItem('authToken');
+ showLogin();
+ } else {
+ alert('Failed to start the game. Please try again.');
+ }
+ } catch (error) {
+ console.error('Error starting game:', error);
+ alert('Failed to start the game. Please try again.');
+ }
+ }
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/templates/index.html b/templates/index.html
index cae234e..5c871d2 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -480,6 +480,10 @@
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded hidden">
Download Game
+
Demo Mode - No Downloads
@@ -660,6 +664,34 @@
alert('Download failed. Please try again.');
}
}
+ async function runGame(gameId) {
+ const token = localStorage.getItem('authToken');
+ if (!token) {
+ showLogin();
+ return;
+ }
+
+ try {
+ const response = await fetch(`/run/${gameId}`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (response.ok) {
+ alert('Game is starting in your DOSBox environment.');
+ } else if (response.status === 401) {
+ localStorage.removeItem('authToken');
+ showLogin();
+ } else {
+ alert('Failed to start the game. Please try again.');
+ }
+ } catch (error) {
+ console.error('Error starting game:', error);
+ alert('Failed to start the game. Please try again.');
+ }
+ }
// Track which covers are being loaded to prevent duplicates
const loadingCovers = new Set();
@@ -937,6 +969,10 @@
if (!currentGameData) return;
await downloadGame(currentGameData.id);
}
+ async function runGameFromOverlay() {
+ if (!currentGameData) return;
+ await runGame(currentGameData.id);
+ }
function openScreenshotModalInOverlay() {
if (!currentGameData || !currentGameData.metadata.screenshot) return;
@@ -989,4 +1025,4 @@
}
});
-{% endblock %}
\ No newline at end of file
+{% endblock %}