mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 06:46:00 +00:00
Merge branch 'master' into trunk-io
This commit is contained in:
@@ -8,9 +8,9 @@ from config import (
|
||||
)
|
||||
from endpoints.responses.heartbeat import HeartbeatResponse
|
||||
from fastapi import APIRouter
|
||||
from handler.github_handler import github_handler
|
||||
from handler.metadata.igdb_handler import IGDB_API_ENABLED
|
||||
from handler.metadata.moby_handler import MOBY_API_ENABLED
|
||||
from utils import get_version
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -24,8 +24,7 @@ def heartbeat() -> HeartbeatResponse:
|
||||
"""
|
||||
|
||||
return {
|
||||
"VERSION": github_handler.get_version(),
|
||||
"NEW_VERSION": github_handler.check_new_version(),
|
||||
"VERSION": get_version(),
|
||||
"ANY_SOURCE_ENABLED": IGDB_API_ENABLED or MOBY_API_ENABLED,
|
||||
"METADATA_SOURCES": {
|
||||
"IGDB_API_ENABLED": IGDB_API_ENABLED,
|
||||
|
||||
@@ -23,7 +23,6 @@ class MetadataSourcesDict(TypedDict):
|
||||
|
||||
class HeartbeatResponse(TypedDict):
|
||||
VERSION: str
|
||||
NEW_VERSION: str
|
||||
WATCHER: WatcherDict
|
||||
SCHEDULER: SchedulerDict
|
||||
ANY_SOURCE_ENABLED: bool
|
||||
|
||||
@@ -9,12 +9,12 @@ class PlatformSchema(BaseModel):
|
||||
id: int
|
||||
slug: str
|
||||
fs_slug: str
|
||||
name: str
|
||||
rom_count: int
|
||||
igdb_id: Optional[int] = None
|
||||
sgdb_id: Optional[int] = None
|
||||
moby_id: Optional[int] = None
|
||||
name: str
|
||||
logo_path: Optional[str] = ""
|
||||
rom_count: int
|
||||
firmware: list[FirmwareSchema] = Field(default_factory=list)
|
||||
|
||||
class Config:
|
||||
|
||||
@@ -34,15 +34,11 @@ class RomNoteSchema(BaseModel):
|
||||
last_edited_at: datetime
|
||||
raw_markdown: str
|
||||
is_public: bool
|
||||
user__username: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@property
|
||||
@computed_field
|
||||
def user__username(self) -> str:
|
||||
return db_user_handler.get_user(self.user_id).username
|
||||
|
||||
@classmethod
|
||||
def for_user(cls, db_rom: Rom, user_id: int) -> list["RomNoteSchema"]:
|
||||
return [
|
||||
|
||||
@@ -157,7 +157,7 @@ def head_rom_content(request: Request, id: int, file_name: str):
|
||||
path=rom_path if not rom.multi else f"{rom_path}/{rom.files[0]}",
|
||||
filename=file_name,
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{rom.name}.zip"',
|
||||
"Content-Disposition": f'attachment; filename="{quote(rom.name)}.zip"',
|
||||
"Content-Type": "application/zip",
|
||||
"Content-Length": str(rom.file_size_bytes),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from handler.github_handler import github_handler
|
||||
from main import app
|
||||
|
||||
from utils import get_version
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@@ -10,7 +11,7 @@ def test_heartbeat():
|
||||
assert response.status_code == 200
|
||||
|
||||
heartbeat = response.json()
|
||||
assert heartbeat.get("VERSION") == github_handler.get_version()
|
||||
assert heartbeat.get("VERSION") == get_version()
|
||||
assert heartbeat.get("WATCHER").get("ENABLED")
|
||||
assert heartbeat.get("WATCHER").get("TITLE") == "Rescan on filesystem change"
|
||||
assert heartbeat.get("SCHEDULER").get("RESCAN").get("ENABLED")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from decorators.database import begin_session
|
||||
from models.firmware import Firmware
|
||||
from sqlalchemy import and_, delete, update
|
||||
from sqlalchemy import update, delete, and_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .base_handler import DBBaseHandler
|
||||
@@ -19,19 +19,19 @@ class DBFirmwareHandler(DBBaseHandler):
|
||||
session: Session = None,
|
||||
) -> Firmware | list[Firmware] | None:
|
||||
return (
|
||||
session.get(Firmware, id)
|
||||
session.scalar(select(Firmware).filter_by(id=id).limit(1))
|
||||
if id
|
||||
else session.query(Firmware).filter_by(platform_id=platform_id).all()
|
||||
else select(Firmware).filter_by(platform_id=platform_id).all()
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def get_firmware_by_filename(
|
||||
self, platform_id: int, file_name: str, session: Session = None
|
||||
) -> Firmware | None:
|
||||
return (
|
||||
session.query(Firmware)
|
||||
):
|
||||
return session.scalar(
|
||||
select(Firmware)
|
||||
.filter_by(platform_id=platform_id, file_name=file_name)
|
||||
.first()
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
@begin_session
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import functools
|
||||
from sqlalchemy import delete, or_, select
|
||||
from sqlalchemy.orm import Session, Query, selectinload
|
||||
|
||||
from decorators.database import begin_session
|
||||
from models.platform import Platform
|
||||
from models.rom import Rom
|
||||
from sqlalchemy import delete, or_
|
||||
from sqlalchemy.orm import Query, Session, joinedload
|
||||
|
||||
from .base_handler import DBBaseHandler
|
||||
|
||||
|
||||
def with_query(func):
|
||||
def with_roms(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))
|
||||
kwargs["query"] = select(Platform).options(
|
||||
selectinload(Platform.roms).load_only(Rom.id)
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -24,32 +26,36 @@ def with_query(func):
|
||||
|
||||
class DBPlatformsHandler(DBBaseHandler):
|
||||
@begin_session
|
||||
@with_query
|
||||
@with_roms
|
||||
def add_platform(
|
||||
self, platform: Platform, query: Query = None, session: Session = None
|
||||
) -> Platform | None:
|
||||
session.merge(platform)
|
||||
platform = session.merge(platform)
|
||||
session.flush()
|
||||
|
||||
return query.filter(Platform.fs_slug == platform.fs_slug).first()
|
||||
return session.scalar(query.filter_by(id=platform.id).limit(1))
|
||||
|
||||
@begin_session
|
||||
@with_query
|
||||
@with_roms
|
||||
def get_platforms(
|
||||
self, id: int | None = None, query: Query = None, session: Session = None
|
||||
) -> list[Platform] | Platform | None:
|
||||
return (
|
||||
query.get(id)
|
||||
session.scalar(query.filter_by(id=id).limit(1))
|
||||
if id
|
||||
else (session.scalars(query.order_by(Platform.name.asc())).unique().all()) # type: ignore[attr-defined]
|
||||
else (
|
||||
session.scalars(select(Platform).order_by(Platform.name.asc()))
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
)
|
||||
|
||||
@begin_session
|
||||
@with_query
|
||||
@with_roms
|
||||
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()
|
||||
return session.scalar(query.filter_by(fs_slug=fs_slug).limit(1))
|
||||
|
||||
@begin_session
|
||||
def delete_platform(self, id: int, session: Session = None) -> int:
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import functools
|
||||
from decorators.database import begin_session
|
||||
from models.rom import Rom, RomNote
|
||||
from sqlalchemy import and_, delete, func, or_, select, update
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, delete, func, select, update, or_, Select
|
||||
from sqlalchemy.orm import Session, Query, selectinload
|
||||
|
||||
from .base_handler import DBBaseHandler
|
||||
|
||||
|
||||
def with_assets(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
session = kwargs.get("session")
|
||||
if session is None:
|
||||
raise ValueError("session is required")
|
||||
|
||||
kwargs["query"] = select(Rom).options(
|
||||
selectinload(Rom.saves),
|
||||
selectinload(Rom.states),
|
||||
selectinload(Rom.screenshots),
|
||||
selectinload(Rom.notes),
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class DBRomsHandler(DBBaseHandler):
|
||||
def _filter(self, data, platform_id: int | None, search_term: str):
|
||||
if platform_id:
|
||||
@@ -33,10 +52,15 @@ class DBRomsHandler(DBBaseHandler):
|
||||
return data.order_by(_column.asc())
|
||||
|
||||
@begin_session
|
||||
def add_rom(self, rom: Rom, session: Session = None) -> Rom:
|
||||
return session.merge(rom)
|
||||
@with_assets
|
||||
def add_rom(self, rom: Rom, query: Query = None, session: Session = None) -> Rom:
|
||||
rom = session.merge(rom)
|
||||
session.flush()
|
||||
|
||||
return session.scalar(query.filter_by(id=rom.id).limit(1))
|
||||
|
||||
@begin_session
|
||||
@with_assets
|
||||
def get_roms(
|
||||
self,
|
||||
id: int | None = None,
|
||||
@@ -44,10 +68,11 @@ class DBRomsHandler(DBBaseHandler):
|
||||
search_term: str = "",
|
||||
order_by: str = "name",
|
||||
order_dir: str = "asc",
|
||||
query: Query = None,
|
||||
session: Session = None,
|
||||
) -> Rom | list[Rom] | None:
|
||||
) -> list[Rom] | Rom | None:
|
||||
return (
|
||||
session.get(Rom, id)
|
||||
session.scalar(query.filter_by(id=id).limit(1))
|
||||
if id
|
||||
else self._order(
|
||||
self._filter(select(Rom), platform_id, search_term),
|
||||
@@ -57,28 +82,35 @@ class DBRomsHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
@with_assets
|
||||
def get_rom_by_filename(
|
||||
self, platform_id: int, file_name: str, session: Session = None
|
||||
self,
|
||||
platform_id: int,
|
||||
file_name: str,
|
||||
query: Query = None,
|
||||
session: Session = None,
|
||||
) -> Rom | None:
|
||||
return session.scalars(
|
||||
select(Rom).filter_by(platform_id=platform_id, file_name=file_name).limit(1)
|
||||
).first()
|
||||
return session.scalar(
|
||||
query.filter_by(platform_id=platform_id, file_name=file_name).limit(1)
|
||||
)
|
||||
|
||||
@begin_session
|
||||
@with_assets
|
||||
def get_rom_by_filename_no_tags(
|
||||
self, file_name_no_tags: str, session: Session = None
|
||||
self, file_name_no_tags: str, query: Query = None, session: Session = None
|
||||
) -> Rom | None:
|
||||
return session.scalars(
|
||||
select(Rom).filter_by(file_name_no_tags=file_name_no_tags).limit(1)
|
||||
).first()
|
||||
return session.scalar(
|
||||
query.filter_by(file_name_no_tags=file_name_no_tags).limit(1)
|
||||
)
|
||||
|
||||
@begin_session
|
||||
@with_assets
|
||||
def get_rom_by_filename_no_ext(
|
||||
self, file_name_no_ext: str, session: Session = None
|
||||
self, file_name_no_ext: str, query: Query = None, session: Session = None
|
||||
) -> Rom | None:
|
||||
return session.scalars(
|
||||
select(Rom).filter_by(file_name_no_ext=file_name_no_ext).limit(1)
|
||||
).first()
|
||||
return session.scalar(
|
||||
query.filter_by(file_name_no_ext=file_name_no_ext).limit(1)
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def update_rom(self, id: int, data: dict, session: Session = None) -> Rom:
|
||||
@@ -90,7 +122,7 @@ class DBRomsHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def delete_rom(self, id: int, session: Session = None) -> None:
|
||||
def delete_rom(self, id: int, session: Session = None) -> Rom:
|
||||
return session.execute(
|
||||
delete(Rom)
|
||||
.where(Rom.id == id)
|
||||
@@ -100,7 +132,7 @@ class DBRomsHandler(DBBaseHandler):
|
||||
@begin_session
|
||||
def purge_roms(
|
||||
self, platform_id: int, roms: list[str], session: Session = None
|
||||
) -> None:
|
||||
) -> int:
|
||||
return session.execute(
|
||||
delete(Rom)
|
||||
.where(and_(Rom.platform_id == platform_id, Rom.file_name.not_in(roms))) # type: ignore[attr-defined]
|
||||
@@ -111,9 +143,9 @@ class DBRomsHandler(DBBaseHandler):
|
||||
def get_rom_note(
|
||||
self, rom_id: int, user_id: int, session: Session = None
|
||||
) -> RomNote | None:
|
||||
return session.scalars(
|
||||
return session.scalar(
|
||||
select(RomNote).filter_by(rom_id=rom_id, user_id=user_id).limit(1)
|
||||
).first()
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def add_rom_note(
|
||||
|
||||
@@ -12,12 +12,8 @@ class DBUsersHandler(DBBaseHandler):
|
||||
return session.merge(user)
|
||||
|
||||
@begin_session
|
||||
def get_user_by_username(
|
||||
self, username: str, session: Session = None
|
||||
) -> User | None:
|
||||
return session.scalars(
|
||||
select(User).filter_by(username=username).limit(1)
|
||||
).first()
|
||||
def get_user_by_username(self, username: str, session: Session = None):
|
||||
return session.scalar(select(User).filter_by(username=username).limit(1))
|
||||
|
||||
@begin_session
|
||||
def get_user(self, id: int, session: Session = None) -> User:
|
||||
@@ -33,7 +29,11 @@ class DBUsersHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def delete_user(self, id: int, session: Session = None) -> None:
|
||||
def get_users(self, session: Session = None):
|
||||
return session.scalars(select(User)).all()
|
||||
|
||||
@begin_session
|
||||
def delete_user(self, id: int, session: Session = None):
|
||||
return session.execute(
|
||||
delete(User)
|
||||
.where(User.id == id)
|
||||
@@ -41,9 +41,5 @@ class DBUsersHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def get_users(self, session: Session = None) -> list[User]:
|
||||
return session.scalars(select(User)).all()
|
||||
|
||||
@begin_session
|
||||
def get_admin_users(self, session: Session = None) -> list[User]:
|
||||
def get_admin_users(self, session: Session = None):
|
||||
return session.scalars(select(User).filter_by(role=Role.ADMIN)).all()
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import subprocess as sp # nosec B404
|
||||
|
||||
import requests
|
||||
from __version__ import __version__
|
||||
from logger.logger import log
|
||||
from packaging.version import InvalidVersion, parse
|
||||
from requests.exceptions import ReadTimeout
|
||||
|
||||
|
||||
class GithubHandler:
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def get_version(self) -> str:
|
||||
"""Returns current version or branch name."""
|
||||
if not __version__ == "<version>":
|
||||
return __version__
|
||||
else:
|
||||
try:
|
||||
output = str(
|
||||
sp.check_output(
|
||||
["git", "branch"], universal_newlines=True
|
||||
) # nosec B603, B607
|
||||
)
|
||||
except (sp.CalledProcessError, FileNotFoundError):
|
||||
return "1.0.0"
|
||||
|
||||
branch = [a for a in output.split("\n") if a.find("*") >= 0][0]
|
||||
return branch[branch.find("*") + 2 :]
|
||||
|
||||
def check_new_version(self) -> str:
|
||||
"""Check for new RomM versions
|
||||
|
||||
Returns:
|
||||
str: New RomM version or empty if in dev mode
|
||||
"""
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://api.github.com/repos/rommapp/romm/releases/latest", timeout=5
|
||||
)
|
||||
except ReadTimeout:
|
||||
log.warning("Timeout while connecting to Github")
|
||||
return ""
|
||||
except requests.exceptions.ConnectionError:
|
||||
log.warning("Could not connect to Github, check your internet connection")
|
||||
return ""
|
||||
try:
|
||||
last_version = response.json()["name"][
|
||||
1:
|
||||
] # remove leading 'v' from 'vX.X.X'
|
||||
except KeyError: # rate limit reached
|
||||
return ""
|
||||
try:
|
||||
if parse(self.get_version()) < parse(last_version):
|
||||
return last_version
|
||||
except InvalidVersion:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
github_handler = GithubHandler()
|
||||
@@ -3,14 +3,19 @@ import sys
|
||||
|
||||
from logger.stdout_formatter import StdoutFormatter
|
||||
|
||||
# Get logger
|
||||
# Set up logger
|
||||
log = logging.getLogger("romm")
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
# Set up sqlachemy logger
|
||||
# sql_log = logging.getLogger("sqlalchemy.engine")
|
||||
# sql_log.setLevel(logging.DEBUG)
|
||||
|
||||
# Define stdout handler
|
||||
stdout_handler = logging.StreamHandler(sys.stdout)
|
||||
stdout_handler.setFormatter(StdoutFormatter())
|
||||
log.addHandler(stdout_handler)
|
||||
# sql_log.addHandler(stdout_handler)
|
||||
|
||||
# Hush passlib warnings
|
||||
logging.getLogger("passlib").setLevel(logging.ERROR)
|
||||
|
||||
@@ -31,9 +31,9 @@ from handler.auth.base_handler import ALGORITHM
|
||||
from handler.auth.hybrid_auth import HybridAuthBackend
|
||||
from handler.auth.middleware import CustomCSRFMiddleware, SessionMiddleware
|
||||
from handler.database import db_user_handler
|
||||
from handler.github_handler import github_handler
|
||||
from handler.socket_handler import socket_handler
|
||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||
from utils import get_version
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -46,7 +46,7 @@ async def lifespan(app: FastAPI):
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(title="RomM API", version=github_handler.get_version(), lifespan=lifespan)
|
||||
app = FastAPI(title="RomM API", version=get_version(), lifespan=lifespan)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
||||
@@ -53,8 +53,8 @@ class Save(RomAsset):
|
||||
|
||||
emulator = Column(String(length=50), nullable=True)
|
||||
|
||||
rom = relationship("Rom", lazy="selectin", back_populates="saves")
|
||||
user = relationship("User", lazy="selectin", back_populates="saves")
|
||||
rom = relationship("Rom", lazy="joined", back_populates="saves")
|
||||
user = relationship("User", lazy="joined", back_populates="saves")
|
||||
|
||||
@cached_property
|
||||
def screenshot(self) -> Optional["Screenshot"]:
|
||||
@@ -74,8 +74,8 @@ class State(RomAsset):
|
||||
|
||||
emulator = Column(String(length=50), nullable=True)
|
||||
|
||||
rom = relationship("Rom", lazy="selectin", back_populates="states")
|
||||
user = relationship("User", lazy="selectin", back_populates="states")
|
||||
rom = relationship("Rom", lazy="joined", back_populates="states")
|
||||
user = relationship("User", lazy="joined", back_populates="states")
|
||||
|
||||
@cached_property
|
||||
def screenshot(self) -> Optional["Screenshot"]:
|
||||
@@ -93,5 +93,5 @@ class Screenshot(RomAsset):
|
||||
__tablename__ = "screenshots"
|
||||
__table_args__ = {"extend_existing": True}
|
||||
|
||||
rom = relationship("Rom", lazy="selectin", back_populates="screenshots")
|
||||
user = relationship("User", lazy="selectin", back_populates="screenshots")
|
||||
rom = relationship("Rom", lazy="joined", back_populates="screenshots")
|
||||
user = relationship("User", lazy="joined", back_populates="screenshots")
|
||||
|
||||
@@ -17,9 +17,7 @@ class Platform(BaseModel):
|
||||
name: str = Column(String(length=400))
|
||||
logo_path: str = Column(String(length=1000), default="")
|
||||
|
||||
roms: Mapped[set[Rom]] = relationship(
|
||||
"Rom", lazy="selectin", back_populates="platform"
|
||||
)
|
||||
roms: Mapped[set[Rom]] = relationship("Rom", back_populates="platform")
|
||||
firmware: Mapped[set[Firmware]] = relationship(
|
||||
"Firmware", lazy="selectin", back_populates="platform"
|
||||
)
|
||||
|
||||
@@ -66,22 +66,17 @@ class Rom(BaseModel):
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
platform = relationship("Platform", lazy="selectin", back_populates="roms")
|
||||
platform = relationship("Platform", lazy="immediate")
|
||||
|
||||
saves: Mapped[list[Save]] = relationship(
|
||||
"Save",
|
||||
lazy="selectin",
|
||||
back_populates="rom",
|
||||
)
|
||||
states: Mapped[list[State]] = relationship(
|
||||
"State", lazy="selectin", back_populates="rom"
|
||||
)
|
||||
states: Mapped[list[State]] = relationship("State", back_populates="rom")
|
||||
screenshots: Mapped[list[Screenshot]] = relationship(
|
||||
"Screenshot", lazy="selectin", back_populates="rom"
|
||||
)
|
||||
notes: Mapped[list["RomNote"]] = relationship(
|
||||
"RomNote", lazy="selectin", back_populates="rom"
|
||||
"Screenshot", back_populates="rom"
|
||||
)
|
||||
notes: Mapped[list["RomNote"]] = relationship("RomNote", back_populates="rom")
|
||||
|
||||
@property
|
||||
def platform_slug(self) -> str:
|
||||
@@ -189,5 +184,9 @@ class RomNote(BaseModel):
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
rom = relationship("Rom", back_populates="notes")
|
||||
user = relationship("User", back_populates="notes")
|
||||
rom = relationship("Rom", lazy="joined", back_populates="notes")
|
||||
user = relationship("User", lazy="joined", back_populates="notes")
|
||||
|
||||
@property
|
||||
def user__username(self) -> str:
|
||||
return self.user.username
|
||||
|
||||
@@ -32,18 +32,13 @@ class User(BaseModel, SimpleUser):
|
||||
|
||||
saves: Mapped[list[Save]] = relationship(
|
||||
"Save",
|
||||
lazy="selectin",
|
||||
back_populates="user",
|
||||
)
|
||||
states: Mapped[list[State]] = relationship(
|
||||
"State", lazy="selectin", back_populates="user"
|
||||
)
|
||||
states: Mapped[list[State]] = relationship("State", back_populates="user")
|
||||
screenshots: Mapped[list[Screenshot]] = relationship(
|
||||
"Screenshot", lazy="selectin", back_populates="user"
|
||||
)
|
||||
notes: Mapped[list[RomNote]] = relationship(
|
||||
"RomNote", lazy="selectin", back_populates="user"
|
||||
"Screenshot", back_populates="user"
|
||||
)
|
||||
notes: Mapped[list[RomNote]] = relationship("RomNote", back_populates="user")
|
||||
|
||||
@property
|
||||
def oauth_scopes(self):
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from __version__ import __version__
|
||||
|
||||
def get_version() -> str:
|
||||
"""Returns current version tag"""
|
||||
if not __version__ == "<version>":
|
||||
return __version__
|
||||
|
||||
return "development"
|
||||
|
||||
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 264 KiB |
@@ -44,7 +44,7 @@ socket.on(
|
||||
|
||||
socket.on("scan:scanning_rom", (rom: Rom) => {
|
||||
scanningStore.set(true);
|
||||
if (romsStore.platform.name === rom.platform_name) {
|
||||
if (romsStore.platformID === rom.platform_id) {
|
||||
romsStore.add([rom]);
|
||||
romsStore.setFiltered(
|
||||
isFiltered ? romsStore.filteredRoms : romsStore.allRoms,
|
||||
@@ -99,32 +99,16 @@ onBeforeUnmount(() => {
|
||||
socket.off("scan:scanning_rom");
|
||||
socket.off("scan:done");
|
||||
socket.off("scan:done_ko");
|
||||
|
||||
document.removeEventListener("network-quiesced", fetchHomeData);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
api.get("/config").then(({ data: data }) => {
|
||||
configStore.set(data);
|
||||
});
|
||||
});
|
||||
|
||||
function fetchHomeData() {
|
||||
// Remove it so it's not called multiple times
|
||||
document.removeEventListener("network-quiesced", fetchHomeData);
|
||||
|
||||
api.get("/heartbeat").then(({ data: data }) => {
|
||||
heartbeat.set(data);
|
||||
});
|
||||
|
||||
platformApi
|
||||
.getPlatforms()
|
||||
.then(({ data: platforms }) => {
|
||||
platformsStore.set(platforms);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
api.get("/config").then(({ data: data }) => {
|
||||
configStore.set(data);
|
||||
});
|
||||
|
||||
userApi
|
||||
.fetchCurrentUser()
|
||||
@@ -134,9 +118,16 @@ function fetchHomeData() {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("network-quiesced", fetchHomeData);
|
||||
platformApi
|
||||
.getPlatforms()
|
||||
.then(({ data: platforms }) => {
|
||||
platformsStore.set(platforms);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -8,10 +8,9 @@ import type { SchedulerDict } from "./SchedulerDict";
|
||||
import type { WatcherDict } from "./WatcherDict";
|
||||
|
||||
export type HeartbeatResponse = {
|
||||
VERSION: string;
|
||||
NEW_VERSION: string;
|
||||
WATCHER: WatcherDict;
|
||||
SCHEDULER: SchedulerDict;
|
||||
ANY_SOURCE_ENABLED: boolean;
|
||||
METADATA_SOURCES: MetadataSourcesDict;
|
||||
VERSION: string;
|
||||
WATCHER: WatcherDict;
|
||||
SCHEDULER: SchedulerDict;
|
||||
ANY_SOURCE_ENABLED: boolean;
|
||||
METADATA_SOURCES: MetadataSourcesDict;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import storeHeartbeat from "@/stores/heartbeat";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import { defaultAvatarPath } from "@/utils";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject, ref } from "vue";
|
||||
import { inject, ref, onBeforeMount } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
// Props
|
||||
@@ -14,18 +14,27 @@ const router = useRouter();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const auth = storeAuth();
|
||||
const heartbeat = storeHeartbeat();
|
||||
const newVersion = heartbeat.value.NEW_VERSION;
|
||||
localStorage.setItem("newVersion", newVersion);
|
||||
const newVersionDismissed = ref(
|
||||
localStorage.getItem("dismissNewVersion") === newVersion
|
||||
);
|
||||
|
||||
// Functions
|
||||
function dismissNewVersion() {
|
||||
localStorage.setItem("dismissNewVersion", newVersion);
|
||||
newVersionDismissed.value = true;
|
||||
const { VERSION } = heartbeat.value;
|
||||
const GITHUB_VERSION = ref(VERSION);
|
||||
const latestVersionDismissed = ref(VERSION === "development");
|
||||
|
||||
function dismissVersionBanner() {
|
||||
localStorage.setItem("dismissedVersion", GITHUB_VERSION.value);
|
||||
latestVersionDismissed.value = true;
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/rommapp/romm/releases/latest"
|
||||
);
|
||||
const json = await response.json();
|
||||
GITHUB_VERSION.value = json.name;
|
||||
latestVersionDismissed.value =
|
||||
VERSION === "development" ||
|
||||
json.name === localStorage.getItem("dismissedVersion");
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
identityApi
|
||||
.logout()
|
||||
@@ -86,7 +95,12 @@ async function logout() {
|
||||
></v-btn>
|
||||
<v-list-item
|
||||
class="bg-terciary py-1 px-1 text-subtitle-2"
|
||||
v-if="newVersion && !newVersionDismissed && !rail"
|
||||
v-if="
|
||||
GITHUB_VERSION &&
|
||||
VERSION !== GITHUB_VERSION &&
|
||||
!latestVersionDismissed &&
|
||||
!rail
|
||||
"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text class="py-2 px-4">
|
||||
@@ -94,18 +108,18 @@ async function logout() {
|
||||
<v-col class="py-1">
|
||||
<span
|
||||
>New version available
|
||||
<span class="text-romm-accent-1">{{ newVersion }}</span></span
|
||||
<span class="text-romm-accent-1">{{ GITHUB_VERSION }}</span></span
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row no-gutters>
|
||||
<v-col class="py-1">
|
||||
<span @click="dismissNewVersion()" class="pointer text-grey"
|
||||
<span @click="dismissVersionBanner()" class="pointer text-grey"
|
||||
>Dismiss</span
|
||||
><span class="ml-4"
|
||||
><a
|
||||
target="_blank"
|
||||
:href="`https://github.com/rommapp/romm/releases/tag/v${newVersion}`"
|
||||
:href="`https://github.com/rommapp/romm/releases/tag/v${GITHUB_VERSION}`"
|
||||
>See what's new!</a
|
||||
></span
|
||||
>
|
||||
|
||||
@@ -7,8 +7,8 @@ import DeleteBtn from "@/components/Gallery/AppBar/DeleteBtn.vue";
|
||||
|
||||
<template>
|
||||
<v-list rounded="0" class="pa-0">
|
||||
<view-firmware-btn />
|
||||
<upload-rom-btn />
|
||||
<view-firmware-btn />
|
||||
<scan-btn />
|
||||
<v-divider class="border-opacity-25" />
|
||||
<delete-btn />
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import storeRoms from "@/stores/roms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { Emitter } from "mitt";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import storePlatforms, { type Platform } from "@/stores/platforms";
|
||||
|
||||
// Props
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const romsStore = storeRoms();
|
||||
const platforms = storePlatforms();
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item
|
||||
v-if="romsStore.platform"
|
||||
v-if="route.params.platform"
|
||||
class="py-4 pr-5 text-romm-red"
|
||||
@click="emitter?.emit('showDeletePlatformDialog', romsStore.platform)"
|
||||
@click="
|
||||
emitter?.emit(
|
||||
'showDeletePlatformDialog',
|
||||
platforms.get(Number(route.params.platform)) as Platform
|
||||
)
|
||||
"
|
||||
>
|
||||
<v-list-item-title class="d-flex">
|
||||
<v-icon icon="mdi-delete" color="red" class="mr-2" />
|
||||
|
||||
@@ -16,7 +16,7 @@ async function scan() {
|
||||
if (!socket.connected) socket.connect();
|
||||
|
||||
socket.emit("scan", {
|
||||
platforms: [romsStore.platform.id],
|
||||
platforms: [romsStore.platformID],
|
||||
type: "quick",
|
||||
apis: heartbeat.getMetadataOptions().map((s) => s.value),
|
||||
});
|
||||
@@ -24,7 +24,7 @@ async function scan() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item v-if="romsStore.platform" @click="scan" class="py-4 pr-5">
|
||||
<v-list-item v-if="romsStore.platformID" @click="scan" class="py-4 pr-5">
|
||||
<v-list-item-title class="d-flex">
|
||||
<v-icon icon="mdi-magnify-scan" class="mr-2" />
|
||||
Scan platform
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { Emitter } from "mitt";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import storeRoms from "@/stores/roms";
|
||||
import storePlatforms, { type Platform } from "@/stores/platforms";
|
||||
|
||||
// Props
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const romsStore = storeRoms();
|
||||
const platforms = storePlatforms();
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item
|
||||
v-if="romsStore.platform"
|
||||
@click="emitter?.emit('showUploadRomDialog', romsStore.platform)"
|
||||
v-if="route.params.platform"
|
||||
@click="
|
||||
emitter?.emit(
|
||||
'showUploadRomDialog',
|
||||
platforms.get(Number(route.params.platform)) as Platform
|
||||
)
|
||||
"
|
||||
class="py-4 pr-5"
|
||||
>
|
||||
<v-list-item-title class="d-flex"
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { inject } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { Emitter } from "mitt";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import storeRoms from "@/stores/roms";
|
||||
import storePlatforms, { type Platform } from "@/stores/platforms";
|
||||
|
||||
// Props
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const romsStore = storeRoms();
|
||||
const platforms = storePlatforms();
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item
|
||||
v-if="romsStore.platform"
|
||||
@click="emitter?.emit('showFirmwareDialog', romsStore.platform)"
|
||||
v-if="route.params.platform"
|
||||
@click="
|
||||
emitter?.emit(
|
||||
'showFirmwareDialog',
|
||||
platforms.get(Number(route.params.platform)) as Platform
|
||||
)
|
||||
"
|
||||
class="py-4 pr-5"
|
||||
>
|
||||
<v-list-item-title class="d-flex"
|
||||
|
||||
@@ -9,7 +9,7 @@ const inflightRequests = new Set();
|
||||
|
||||
const networkQuiesced = debounce(() => {
|
||||
document.dispatchEvent(new CustomEvent("network-quiesced"));
|
||||
}, 300);
|
||||
}, 250);
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
// Add request to set of inflight requests
|
||||
|
||||
@@ -125,7 +125,7 @@ async function downloadRom({
|
||||
}
|
||||
|
||||
export type UpdateRom = Rom & {
|
||||
artwork?: File[];
|
||||
artwork?: File;
|
||||
};
|
||||
|
||||
async function updateRom({
|
||||
@@ -144,7 +144,7 @@ async function updateRom({
|
||||
formData.append("file_name", rom.file_name);
|
||||
formData.append("summary", rom.summary || "");
|
||||
formData.append("url_cover", rom.url_cover || "");
|
||||
if (rom.artwork) formData.append("artwork", rom.artwork[0]);
|
||||
if (rom.artwork) formData.append("artwork", rom.artwork);
|
||||
|
||||
return api.put(`/roms/${rom.id}`, formData, {
|
||||
params: { rename_as_igdb: renameAsIGDB, remove_cover: removeCover },
|
||||
|
||||
@@ -33,7 +33,7 @@ async function updateUser({
|
||||
avatar,
|
||||
...attrs
|
||||
}: UserSchema & {
|
||||
avatar?: File[];
|
||||
avatar?: File;
|
||||
password?: string;
|
||||
}): Promise<{ data: UserSchema }> {
|
||||
return api.put(
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineStore("heartbeat", {
|
||||
|
||||
actions: {
|
||||
set(data: HeartbeatResponse) {
|
||||
this.value = data;
|
||||
this.value = { ...this.value, ...data };
|
||||
},
|
||||
getMetadataOptions() {
|
||||
return computed(() => [
|
||||
|
||||
@@ -13,7 +13,7 @@ export type Rom = RomSchema & {
|
||||
|
||||
export default defineStore("roms", {
|
||||
state: () => ({
|
||||
_platform: {} as PlatformSchema,
|
||||
_platformID: 0,
|
||||
_all: [] as Rom[],
|
||||
_grouped: [] as Rom[],
|
||||
_filteredIDs: [] as number[],
|
||||
@@ -27,7 +27,7 @@ export default defineStore("roms", {
|
||||
}),
|
||||
|
||||
getters: {
|
||||
platform: (state) => state._platform,
|
||||
platformID: (state) => state._platformID,
|
||||
allRoms: (state) => state._all,
|
||||
filteredRoms: (state) =>
|
||||
state._grouped.filter((rom) => state._filteredIDs.includes(rom.id)),
|
||||
@@ -69,8 +69,8 @@ export default defineStore("roms", {
|
||||
return a.sort_comparator.localeCompare(b.sort_comparator);
|
||||
});
|
||||
},
|
||||
setPlatform(platform: PlatformSchema) {
|
||||
this._platform = platform;
|
||||
setPlatformID(platformID: number) {
|
||||
this._platformID = platformID;
|
||||
},
|
||||
setRecentRoms(roms: Rom[]) {
|
||||
this.recentRoms = roms;
|
||||
|
||||
2
frontend/src/types/emitter.d.ts
vendored
2
frontend/src/types/emitter.d.ts
vendored
@@ -5,7 +5,7 @@ import type { User } from "@/stores/users";
|
||||
|
||||
export type UserItem = User & {
|
||||
password: string;
|
||||
avatar?: File[];
|
||||
avatar?: File;
|
||||
};
|
||||
|
||||
export type SnackbarStatus = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import romApi from "@/services/api/rom";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import storeRoms from "@/stores/roms";
|
||||
import storePlatforms from "@/stores/platforms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { RomSelectEvent } from "@/types/rom";
|
||||
import { normalizeString, toTop, views } from "@/utils";
|
||||
@@ -23,6 +24,7 @@ const galleryFilterStore = storeGalleryFilter();
|
||||
const gettingRoms = ref(false);
|
||||
const fabMenu = ref(false);
|
||||
const scrolledToTop = ref(true);
|
||||
const platforms = storePlatforms();
|
||||
const romsStore = storeRoms();
|
||||
const {
|
||||
allRoms,
|
||||
@@ -31,7 +33,7 @@ const {
|
||||
searchRoms,
|
||||
cursor,
|
||||
searchCursor,
|
||||
platform,
|
||||
platformID,
|
||||
} = storeToRefs(romsStore);
|
||||
|
||||
// Event listeners bus
|
||||
@@ -58,7 +60,7 @@ async function fetchRoms() {
|
||||
|
||||
await romApi
|
||||
.getRoms({
|
||||
platformId: platform.value.id,
|
||||
platformId: platformID.value,
|
||||
cursor: galleryFilterStore.isFiltered()
|
||||
? searchCursor.value
|
||||
: cursor.value,
|
||||
@@ -82,12 +84,12 @@ async function fetchRoms() {
|
||||
})
|
||||
.catch((error) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Couldn't fetch roms for ${platform.value.name}: ${error}`,
|
||||
msg: `Couldn't fetch roms for platform ID ${platformID.value}: ${error}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
timeout: 4000,
|
||||
});
|
||||
console.error(`Couldn't fetch roms for ${platform.value.name}: ${error}`);
|
||||
console.error(`Couldn't fetch roms for platform ID ${platformID.value}: ${error}`);
|
||||
})
|
||||
.finally(() => {
|
||||
gettingRoms.value = false;
|
||||
@@ -184,13 +186,19 @@ function onScroll() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const { data: platform } = await platformApi.getPlatform(
|
||||
Number(route.params.platform)
|
||||
);
|
||||
romsStore.setPlatform(platform);
|
||||
const storedPlatformID = romsStore.platformID;
|
||||
const platformID = Number(route.params.platform);
|
||||
|
||||
romsStore.setPlatformID(platformID);
|
||||
|
||||
const platform = platforms.get(platformID);
|
||||
if (!platform) {
|
||||
const { data } = await platformApi.getPlatform(platformID)
|
||||
platforms.add(data);
|
||||
}
|
||||
|
||||
// If platform is different, reset store and fetch roms
|
||||
if (platform.id != romsStore.platform.id) {
|
||||
if (storedPlatformID != platformID) {
|
||||
resetGallery();
|
||||
await fetchRoms();
|
||||
}
|
||||
@@ -219,10 +227,16 @@ onBeforeRouteUpdate(async (to, _) => {
|
||||
// Triggers when change query param of the same route
|
||||
// Reset store if switching to another platform
|
||||
resetGallery();
|
||||
const { data: newPlatform } = await platformApi.getPlatform(
|
||||
Number(to.params.platform)
|
||||
);
|
||||
romsStore.setPlatform(newPlatform);
|
||||
|
||||
const platformID = Number(to.params.platform);
|
||||
romsStore.setPlatformID(platformID);
|
||||
|
||||
const platform = platforms.get(platformID);
|
||||
if (!platform) {
|
||||
const { data } = await platformApi.getPlatform(platformID)
|
||||
platforms.add(data);
|
||||
}
|
||||
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
});
|
||||
|
||||
@@ -100,7 +100,6 @@ watch(metadataOptions, (newOptions) => {
|
||||
:items="platforms.value"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
rounded="0"
|
||||
multiple
|
||||
return-object
|
||||
clearable
|
||||
@@ -138,7 +137,6 @@ watch(metadataOptions, (newOptions) => {
|
||||
return-object
|
||||
clearable
|
||||
hide-details
|
||||
rounded="0"
|
||||
chips
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
|
||||
Reference in New Issue
Block a user