Ready to merge

This commit is contained in:
2025-09-08 20:02:20 -04:00
parent 5d837c5501
commit daaa0b5fee
7 changed files with 142 additions and 21 deletions

View File

@@ -42,6 +42,7 @@
passlib
alembic
aiohttp
websockets
'';
};
# uv = {

View File

@@ -137,7 +137,6 @@ async def filter_new_roms(romlist: Roms, session: Session) -> 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,
@@ -148,9 +147,6 @@ 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

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -38,6 +38,10 @@
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded">
Download Game
</button>
<button onclick="runGame({{ game.id }})"
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded">
Run Game
</button>
{% else %}
<span class="bg-gray-600 px-4 py-2 rounded cursor-not-allowed">
{% 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.');
}
}
</script>
{% endblock %}

View File

@@ -480,6 +480,10 @@
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded hidden">
Download Game
</button>
<button onclick="runGameFromOverlay()"
class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded">
Run Game
</button>
<span id="gameDownloadDisabled" class="bg-gray-600 px-4 py-2 rounded cursor-not-allowed hidden">
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;