diff --git a/backend/config/__init__.py b/backend/config/__init__.py
index a4e7296f3..064f94254 100644
--- a/backend/config/__init__.py
+++ b/backend/config/__init__.py
@@ -6,10 +6,11 @@
load_dotenv()
-# UVICORN
+# GUNICORN
DEV_PORT: Final = int(os.environ.get("VITE_BACKEND_DEV_PORT", "5000"))
DEV_HOST: Final = "0.0.0.0"
ROMM_HOST: Final = os.environ.get("ROMM_HOST", DEV_HOST)
+GUNICORN_WORKERS: Final = int(os.environ.get("GUNICORN_WORKERS", 2))
# PATHS
ROMM_BASE_PATH: Final = os.environ.get("ROMM_BASE_PATH", "/romm")
diff --git a/backend/endpoints/platform.py b/backend/endpoints/platform.py
index d0b1da142..96327afe3 100644
--- a/backend/endpoints/platform.py
+++ b/backend/endpoints/platform.py
@@ -88,9 +88,7 @@ def get_platform(request: Request, id: int) -> PlatformSchema:
PlatformSchema: Platform
"""
- return PlatformSchema.from_orm_with_request(
- db_platform_handler.get_platforms(id), request
- )
+ return db_platform_handler.get_platforms(id)
@protected_route(router.put, "/platforms/{id}", ["platforms.write"])
diff --git a/backend/endpoints/responses/platform.py b/backend/endpoints/responses/platform.py
index 4c2fc4de4..5164a343f 100644
--- a/backend/endpoints/responses/platform.py
+++ b/backend/endpoints/responses/platform.py
@@ -1,8 +1,6 @@
from typing import Optional
from pydantic import BaseModel, Field
-from fastapi import Request
-from models.platform import Platform
from .firmware import FirmwareSchema
@@ -16,19 +14,7 @@ class PlatformSchema(BaseModel):
name: str
logo_path: Optional[str] = ""
rom_count: int
-
- firmware_files: list[FirmwareSchema] = Field(default_factory=list)
+ firmware: list[FirmwareSchema] = Field(default_factory=list)
class Config:
from_attributes = True
-
- @classmethod
- def from_orm_with_request(
- cls, db_platform: Platform, request: Request
- ) -> "PlatformSchema":
- platform = cls.model_validate(db_platform)
- platform.firmware_files = [
- FirmwareSchema.model_validate(f)
- for f in sorted(db_platform.firmware, key=lambda x: x.file_name)
- ]
- return platform
diff --git a/backend/handler/database/platforms_handler.py b/backend/handler/database/platforms_handler.py
index 1b48d5a61..e783987fd 100644
--- a/backend/handler/database/platforms_handler.py
+++ b/backend/handler/database/platforms_handler.py
@@ -1,37 +1,58 @@
+import functools
+from sqlalchemy import delete, or_
+from sqlalchemy.orm import Session, Query, joinedload
+
from decorators.database import begin_session
from models.platform import Platform
from models.rom import Rom
-from sqlalchemy import delete, func, or_, select
-from sqlalchemy.orm import Session
from .base_handler import DBBaseHandler
+def with_query(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ session = kwargs.get("session")
+ if session is None:
+ raise ValueError("session is required")
+
+ kwargs["query"] = session.query(Platform).options(joinedload(Platform.roms))
+ return func(*args, **kwargs)
+
+ return wrapper
+
+
class DBPlatformsHandler(DBBaseHandler):
@begin_session
- def add_platform(self, platform: Platform, session: Session = None):
- return session.merge(platform)
+ @with_query
+ def add_platform(
+ self, platform: Platform, query: Query = None, session: Session = None
+ ) -> Platform | None:
+ session.merge(platform)
+ session.flush()
+
+ return query.filter(Platform.fs_slug == platform.fs_slug).first()
@begin_session
- def get_platforms(self, id: int = None, session: Session = None):
+ @with_query
+ def get_platforms(
+ self, id: int = None, query: Query = None, session: Session = None
+ ) -> list[Platform] | Platform | None:
return (
- session.get(Platform, id)
+ query.get(id)
if id
- else (
- session.scalars(select(Platform).order_by(Platform.name.asc()))
- .unique()
- .all()
- )
+ else (session.scalars(query.order_by(Platform.name.asc())).unique().all())
)
@begin_session
- def get_platform_by_fs_slug(self, fs_slug: str, session: Session = None):
- return session.scalars(
- select(Platform).filter_by(fs_slug=fs_slug).limit(1)
- ).first()
+ @with_query
+ def get_platform_by_fs_slug(
+ self, fs_slug: str, query: Query = None, session: Session = None
+ ) -> Platform | None:
+ return session.scalars(query.filter_by(fs_slug=fs_slug).limit(1)).first()
@begin_session
- def delete_platform(self, id: int, session: Session = None):
+ def delete_platform(self, id: int, session: Session = None) -> int:
# Remove all roms from that platforms first
session.execute(
delete(Rom)
@@ -45,13 +66,7 @@ def delete_platform(self, id: int, session: Session = None):
)
@begin_session
- def get_rom_count(self, platform_id: int, session: Session = None):
- return session.scalar(
- select(func.count()).select_from(Rom).filter_by(platform_id=platform_id)
- )
-
- @begin_session
- def purge_platforms(self, fs_platforms: list[str], session: Session = None):
+ def purge_platforms(self, fs_platforms: list[str], session: Session = None) -> int:
return session.execute(
delete(Platform)
.where(or_(Platform.fs_slug.not_in(fs_platforms), Platform.slug.is_(None)))
diff --git a/backend/models/platform.py b/backend/models/platform.py
index 8709b407a..520fa84f5 100644
--- a/backend/models/platform.py
+++ b/backend/models/platform.py
@@ -1,8 +1,9 @@
+from sqlalchemy import Column, Integer, String, select, func
+from sqlalchemy.orm import Mapped, relationship, column_property
+
from models.base import BaseModel
from models.rom import Rom
from models.firmware import Firmware
-from sqlalchemy import Column, Integer, String
-from sqlalchemy.orm import Mapped, relationship
class Platform(BaseModel):
@@ -24,11 +25,10 @@ class Platform(BaseModel):
"Firmware", lazy="selectin", back_populates="platform"
)
- @property
- def rom_count(self) -> int:
- from handler.database import db_platform_handler
-
- return db_platform_handler.get_rom_count(self.id)
+ # This runs a subquery to get the count of roms for the platform
+ rom_count = column_property(
+ select(func.count(Rom.id)).where(Rom.platform_id == id).scalar_subquery()
+ )
def __repr__(self) -> str:
return self.name
diff --git a/docker/init_scripts/init b/docker/init_scripts/init
index ff0a3be14..d0437a983 100755
--- a/docker/init_scripts/init
+++ b/docker/init_scripts/init
@@ -46,7 +46,7 @@ start_bin_gunicorn () {
--bind=0.0.0.0:5000 \
--bind=unix:/tmp/gunicorn.sock \
--pid=/tmp/gunicorn.pid \
- --workers 2 \
+ --workers ${GUNICORN_WORKERS:=2} \
main:app &
}
diff --git a/env.template b/env.template
index 8a7701189..92a62393e 100644
--- a/env.template
+++ b/env.template
@@ -1,6 +1,9 @@
ROMM_BASE_PATH=/path/to/romm_mock
VITE_BACKEND_DEV_PORT=5000
+
+# Gunicorn (optional)
ROMM_HOST=localhost
+GUNICORN_WORKERS=4 # (2 × CPU cores) + 1
# IGDB credentials
IGDB_CLIENT_ID=
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 0241405af..5acbfc9b0 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,17 +1,21 @@
diff --git a/frontend/src/__generated__/models/PlatformSchema.ts b/frontend/src/__generated__/models/PlatformSchema.ts
index 5a6997575..89098c2aa 100644
--- a/frontend/src/__generated__/models/PlatformSchema.ts
+++ b/frontend/src/__generated__/models/PlatformSchema.ts
@@ -15,5 +15,5 @@ export type PlatformSchema = {
name: string;
logo_path?: (string | null);
rom_count: number;
- firmware_files?: Array;
+ firmware?: Array;
};
diff --git a/frontend/src/components/Details/Emulation.vue b/frontend/src/components/Details/Emulation.vue
index 530b9eecf..915005a6f 100644
--- a/frontend/src/components/Details/Emulation.vue
+++ b/frontend/src/components/Details/Emulation.vue
@@ -44,7 +44,7 @@ function onFullScreenChange() {
label="BIOS"
v-model="biosRef"
:items="
- props.platform.firmware_files?.map((f) => ({
+ props.platform.firmware?.map((f) => ({
title: f.file_name,
value: f,
})) ?? []
diff --git a/frontend/src/components/Dialog/Platform/ViewFirmware.vue b/frontend/src/components/Dialog/Platform/ViewFirmware.vue
index 3bb5139c9..e16ebe90d 100644
--- a/frontend/src/components/Dialog/Platform/ViewFirmware.vue
+++ b/frontend/src/components/Dialog/Platform/ViewFirmware.vue
@@ -45,7 +45,7 @@ function uploadFirmware() {
.then(({ data }) => {
const { uploaded, firmware } = data;
if (selectedPlatform.value) {
- selectedPlatform.value.firmware_files = firmware;
+ selectedPlatform.value.firmware = firmware;
}
emitter?.emit("snackbarShow", {
@@ -75,8 +75,8 @@ function deleteFirmware() {
.deleteFirmware({ firmware: selectedFirmware.value, deleteFromFs: false })
.then(() => {
if (selectedPlatform.value) {
- selectedPlatform.value.firmware_files =
- selectedPlatform.value.firmware_files?.filter(
+ selectedPlatform.value.firmware =
+ selectedPlatform.value.firmware?.filter(
(firmware) => !selectedFirmware.value.includes(firmware)
);
}
@@ -91,12 +91,12 @@ function deleteFirmware() {
}
function allFirmwareSelected() {
- if (selectedPlatform.value?.firmware_files?.length == 0) {
+ if (selectedPlatform.value?.firmware?.length == 0) {
return false;
}
return (
selectedFirmware.value.length ===
- selectedPlatform.value?.firmware_files?.length
+ selectedPlatform.value?.firmware?.length
);
}
@@ -104,7 +104,7 @@ function selectAllFirmware() {
if (allFirmwareSelected()) {
selectedFirmware.value = [];
} else {
- selectedFirmware.value = selectedPlatform.value?.firmware_files ?? [];
+ selectedFirmware.value = selectedPlatform.value?.firmware ?? [];
}
}
@@ -223,14 +223,14 @@ function selectAllFirmware() {
@@ -286,8 +286,8 @@ function selectAllFirmware() {
variant="text"
:disabled="
!(
- selectedPlatform?.firmware_files != undefined &&
- selectedPlatform?.firmware_files?.length > 0
+ selectedPlatform?.firmware != undefined &&
+ selectedPlatform?.firmware?.length > 0
)
"
>
diff --git a/frontend/src/services/api/filter.ts b/frontend/src/services/api/filter.ts
deleted file mode 100644
index c8783a302..000000000
--- a/frontend/src/services/api/filter.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import api from "@/services/api/index";
-
-export const stateApi = api;
-
-async function getFilters(): Promise<{ data: { genres: string[] } }> {
- return api.get("/filters");
-}
-export default {
- getFilters,
-};
diff --git a/frontend/src/services/api/index.ts b/frontend/src/services/api/index.ts
index be44dd001..8002d8e86 100644
--- a/frontend/src/services/api/index.ts
+++ b/frontend/src/services/api/index.ts
@@ -1,18 +1,40 @@
import axios from "axios";
import cookie from "js-cookie";
-
import router from "@/plugins/router";
+import { debounce } from "lodash";
const api = axios.create({ baseURL: "/api", timeout: 120000 });
-// Set CSRF header for all requests
+const inflightRequests = new Set();
+
+const networkQuiesced = debounce(() => {
+ document.dispatchEvent(new CustomEvent("network-quiesced"));
+}, 300);
+
api.interceptors.request.use((config) => {
+ // Add request to set of inflight requests
+ inflightRequests.add(config.url);
+
+ // Cancel debounced networkQuiesced since a new request just came in
+ networkQuiesced.cancel();
+
+ // Set CSRF header for all requests
config.headers["x-csrftoken"] = cookie.get("csrftoken");
return config;
});
api.interceptors.response.use(
- (response) => response,
+ (response) => {
+ // Remove request from set of inflight requests
+ inflightRequests.delete(response.config.url);
+
+ // If there are no more inflight requests, fetch app-wide data
+ if (inflightRequests.size === 0) {
+ networkQuiesced();
+ }
+
+ return response;
+ },
(error) => {
if (error.response?.status === 403) {
router.push({
diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue
index feee1043f..90fe6a8d5 100644
--- a/frontend/src/views/Home.vue
+++ b/frontend/src/views/Home.vue
@@ -20,14 +20,12 @@ import DeleteUserDialog from "@/components/Dialog/User/DeleteUser.vue";
import EditUserDialog from "@/components/Dialog/User/EditUser.vue";
import Drawer from "@/components/Drawer/Base.vue";
import platformApi from "@/services/api/platform";
-import userApi from "@/services/api/user";
-import storeAuth from "@/stores/auth";
import storePlatforms from "@/stores/platforms";
import storeScanning from "@/stores/scanning";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { storeToRefs } from "pinia";
-import { inject, onMounted, ref } from "vue";
+import { inject, ref } from "vue";
import { useDisplay } from "vuetify";
// Props
@@ -35,7 +33,6 @@ const { mdAndDown } = useDisplay();
const scanningStore = storeScanning();
const { scanning } = storeToRefs(scanningStore);
const platformsStore = storePlatforms();
-const auth = storeAuth();
const refreshView = ref(0);
// Event listeners bus
@@ -47,27 +44,6 @@ emitter?.on("refreshDrawer", async () => {
emitter?.on("refreshView", async () => {
refreshView.value = refreshView.value + 1;
});
-
-// Functions
-onMounted(() => {
- platformApi
- .getPlatforms()
- .then(({ data: platforms }) => {
- platformsStore.set(platforms);
- })
- .catch((error) => {
- console.error(error);
- });
-
- userApi
- .fetchCurrentUser()
- .then(({ data: user }) => {
- auth.setUser(user);
- })
- .catch((error) => {
- console.error(error);
- });
-});