diff --git a/.github/actions/build.sh b/.github/actions/build.sh index 6ee7208cc..ea469a440 100755 --- a/.github/actions/build.sh +++ b/.github/actions/build.sh @@ -1,6 +1,6 @@ -if [[ $GIT_BRANCH = 'develop' ]]; then +if [[ $GIT_BRANCH != 'master' ]]; then docker buildx build --push\ - --tag zurdi15/romm:dev-${VERSION}\ + --tag zurdi15/romm:dev-latest --tag zurdi15/romm:dev-${VERSION}\ --platform linux/arm64 . --file ./docker/Dockerfile else docker buildx build --push\ diff --git a/.gitignore b/.gitignore index ea62aef3b..a47e7fe79 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,7 @@ envs.env mariadb # data test -library \ No newline at end of file +library + +# config test +romm \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 5e3007f53..d932e3c95 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -2,4 +2,5 @@ requests==2.28.2 fastapi==0.92.0 uvicorn==0.20.0 mariadb==1.1.6 -SQLAlchemy==2.0.7 \ No newline at end of file +SQLAlchemy==2.0.7 +PyYAML==6.0 \ No newline at end of file diff --git a/backend/src/config/__init__.py b/backend/src/config/__init__.py new file mode 100644 index 000000000..5d3d7f6b0 --- /dev/null +++ b/backend/src/config/__init__.py @@ -0,0 +1,39 @@ +import os +import pathlib +import yaml +from yaml.loader import SafeLoader + + +# Uvicorn +DEV_PORT: int = 5000 +DEV_HOST: str = "0.0.0.0" + +# PATHS +LIBRARY_BASE_PATH: str = f"{pathlib.Path(__file__).parent.parent.parent.parent.resolve()}/library" +ROMM_USER_CONFIG_PATH: str = f"{pathlib.Path(__file__).parent.parent.parent.parent.resolve()}/romm/config.yml" + +# ROMM RESERVED FOLDERS +RESERVED_FOLDERS: list = ['resources', 'database'] + +# DEFAULT RESOURCES +DEFAULT_URL_COVER_L: str = "https://images.igdb.com/igdb/image/upload/t_cover_big/nocover.png" +DEFAULT_PATH_COVER_L: str = f"/assets/library/resources/default/cover_l.png" +DEFAULT_URL_COVER_S: str = "https://images.igdb.com/igdb/image/upload/t_cover_small/nocover.png" +DEFAULT_PATH_COVER_S: str = f"/assets/library/resources/default/cover_s.png" + +# IGDB +CLIENT_ID: str = os.getenv('CLIENT_ID') +CLIENT_SECRET: str = os.getenv('CLIENT_SECRET') +# STEAMGRIDDB +STEAMGRIDDB_API_KEY: str = os.getenv('STEAMGRIDDB_API_KEY') + +# USER CONFIG +try: + with open(ROMM_USER_CONFIG_PATH) as config: config = yaml.load(config, Loader=SafeLoader) +except FileNotFoundError: + config = None +user_config: dict = {} if not config else config + +# DB DRIVERS +SUPPORTED_DB_DRIVERS: list = ['sqlite', 'mariadb'] +ROMM_DB_DRIVER: str = os.getenv('ROMM_DB_DRIVER', 'sqlite') diff --git a/backend/src/config/config.py b/backend/src/config/config_loader.py similarity index 50% rename from backend/src/config/config.py rename to backend/src/config/config_loader.py index f3ffa72e0..6aa703eea 100644 --- a/backend/src/config/config.py +++ b/backend/src/config/config_loader.py @@ -1,38 +1,9 @@ import os import sys -import pathlib - from urllib.parse import quote_plus -from logger.logger import log - -# Uvicorn -DEV_PORT: int = 5000 -DEV_HOST: str = "0.0.0.0" - -# PATHS -LIBRARY_BASE_PATH: str = f"{pathlib.Path(__file__).parent.parent.parent.parent.resolve()}/library" - -DEFAULT_URL_LOGO: str = "https://images.igdb.com/igdb/image/upload/t_cover_big/nocover.png" -DEFAULT_PATH_LOGO: str = f"/assets/library/resources/default/logo_l.png" - -DEFAULT_URL_COVER_L: str = "https://images.igdb.com/igdb/image/upload/t_cover_big/nocover.png" -DEFAULT_PATH_COVER_L: str = f"/assets/library/resources/default/cover_l.png" -DEFAULT_URL_COVER_S: str = "https://images.igdb.com/igdb/image/upload/t_cover_small/nocover.png" -DEFAULT_PATH_COVER_S: str = f"/assets/library/resources/default/cover_s.png" -# IGDB -CLIENT_ID: str = os.getenv('CLIENT_ID') -CLIENT_SECRET: str = os.getenv('CLIENT_SECRET') -# STEAMGRIDDB -STEAMGRIDDB_API_KEY: str = os.getenv('STEAMGRIDDB_API_KEY') - - -RESERVED_FOLDERS: list = ['resources', 'database'] - - -# DB DRIVERS -SUPPORTED_DB_DRIVERS: list = ['sqlite', 'mariadb'] -ROMM_DB_DRIVER: str = os.getenv('ROMM_DB_DRIVER', 'sqlite') +from config import ROMM_DB_DRIVER, SUPPORTED_DB_DRIVERS, LIBRARY_BASE_PATH +from logger.logger import log def get_db_engine(): diff --git a/backend/src/handler/db_handler.py b/backend/src/handler/db_handler.py index 8348f9e91..b8f1f8e52 100644 --- a/backend/src/handler/db_handler.py +++ b/backend/src/handler/db_handler.py @@ -1,4 +1,5 @@ import functools +import json from fastapi import HTTPException from sqlalchemy import select @@ -43,22 +44,22 @@ def add_rom(self, rom: Rom) -> None: def get_roms(self, p_slug: str) -> list[Rom]: with Session.begin() as session: - return session.scalars(select(Rom).filter_by(p_slug=p_slug).order_by(Rom.filename.asc())).all() + return session.scalars(select(Rom).filter_by(p_slug=p_slug).order_by(Rom.file_name.asc())).all() - def get_rom(self, p_slug: str, filename: str) -> Rom: + def get_rom(self, p_slug: str, file_name: str) -> Rom: with Session.begin() as session: - return session.scalars(select(Rom).filter_by(p_slug=p_slug, filename=filename)).first() + return session.scalars(select(Rom).filter_by(p_slug=p_slug, file_name=file_name)).first() - def update_rom(self, p_slug: str, filename: str, data: dict) -> None: + def update_rom(self, p_slug: str, file_name: str, data: dict) -> None: with Session.begin() as session: session.query(Rom) \ - .filter(Rom.p_slug==p_slug, Rom.filename==filename) \ + .filter(Rom.p_slug==p_slug, Rom.file_name==file_name) \ .update(data, synchronize_session='evaluate') - def delete_rom(self, p_slug: str, filename: str) -> None: + def delete_rom(self, p_slug: str, file_name: str) -> None: with Session.begin() as session: session.query(Rom) \ - .filter(Rom.p_slug==p_slug, Rom.filename==filename) \ + .filter(Rom.p_slug==p_slug, Rom.file_name==file_name) \ .delete(synchronize_session='evaluate') def purge_platforms(self, platforms: list[str]) -> None: @@ -72,5 +73,5 @@ def purge_roms(self, p_slug: str, roms: list[dict]) -> None: log.info(f"Purging {p_slug} roms") with Session.begin() as session: session.query(Rom) \ - .filter(Rom.p_slug==p_slug, Rom.filename.not_in([rom['filename'] for rom in roms])) \ + .filter(Rom.p_slug==p_slug, Rom.file_name.not_in([rom['file_name'] for rom in roms])) \ .delete(synchronize_session='evaluate') diff --git a/backend/src/handler/igdb_handler.py b/backend/src/handler/igdb_handler.py index 2d9917d15..bca3762cb 100644 --- a/backend/src/handler/igdb_handler.py +++ b/backend/src/handler/igdb_handler.py @@ -5,7 +5,7 @@ import requests -from config.config import CLIENT_ID, CLIENT_SECRET, DEFAULT_URL_COVER_L +from config import CLIENT_ID, CLIENT_SECRET, DEFAULT_URL_COVER_L from logger.logger import log @@ -44,8 +44,8 @@ def get_platform_details(self, slug: str) -> tuple: @check_twitch_token - def get_rom_details(self, filename: str, p_igdb_id: int, r_igdb_id_search: str) -> dict: - filename_no_ext: str = filename.split('.')[0] + def get_rom_details(self, file_name: str, p_igdb_id: int, r_igdb_id_search: str) -> dict: + file_name_no_tags: str = re.sub('[\(\[].*?[\)\]]', '', file_name.split('.')[0]) r_igdb_id: str = "" slug: str = "" name: str = "" @@ -65,11 +65,10 @@ def get_rom_details(self, filename: str, p_igdb_id: int, r_igdb_id_search: str) else: if p_igdb_id: - search_term: str = re.sub('[\(\[].*?[\)\]]', '', filename_no_ext) try: res_details: dict = requests.post("https://api.igdb.com/v4/games/", headers=self.headers, - data=f"search \"{search_term}\";fields id, slug, name, summary; where platforms=[{p_igdb_id}] & category=0;").json()[0] + data=f"search \"{file_name_no_tags}\";fields id, slug, name, summary; where platforms=[{p_igdb_id}] & category=0;").json()[0] r_igdb_id = res_details['id'] slug = res_details['slug'] name = res_details['name'] @@ -80,7 +79,7 @@ def get_rom_details(self, filename: str, p_igdb_id: int, r_igdb_id_search: str) except IndexError: try: res_details: dict = requests.post("https://api.igdb.com/v4/games/", headers=self.headers, - data=f"search \"{search_term}\";fields name, id, slug, summary; where platforms=[{p_igdb_id}] & category=10;").json()[0] + data=f"search \"{file_name_no_tags}\";fields name, id, slug, summary; where platforms=[{p_igdb_id}] & category=10;").json()[0] r_igdb_id = res_details['id'] slug = res_details['slug'] name = res_details['name'] @@ -91,7 +90,7 @@ def get_rom_details(self, filename: str, p_igdb_id: int, r_igdb_id_search: str) except IndexError: try: res_details: dict = requests.post("https://api.igdb.com/v4/games/", headers=self.headers, - data=f"search \"{search_term}\";fields name, id, slug, summary; where platforms=[{p_igdb_id}];").json()[0] + data=f"search \"{file_name_no_tags}\";fields name, id, slug, summary; where platforms=[{p_igdb_id}];").json()[0] r_igdb_id = res_details['id'] slug = res_details['slug'] name = res_details['name'] @@ -100,7 +99,7 @@ def get_rom_details(self, filename: str, p_igdb_id: int, r_igdb_id_search: str) except KeyError: pass except IndexError: - log.warning(f"{filename} rom not found in igdb") + log.warning(f"{file_name} rom not found in igdb") if r_igdb_id: try: res_details: dict = requests.post("https://api.igdb.com/v4/covers/", headers=self.headers, @@ -108,16 +107,16 @@ def get_rom_details(self, filename: str, p_igdb_id: int, r_igdb_id_search: str) url_cover: str = f"https:{res_details['url']}" except IndexError: log.warning(f"{name} cover not found in igdb") - if not name: name = filename_no_ext - return (r_igdb_id, filename_no_ext, slug, name, summary, url_cover) + if not name: name = file_name_no_tags + return (r_igdb_id, file_name_no_tags, slug, name, summary, url_cover) @check_twitch_token - def get_matched_roms(self, filename: str, p_igdb_id: int) -> list: - search_term: str = re.sub('[\(\[].*?[\)\]]', '', filename.split('.')[0]) + def get_matched_roms(self, file_name: str, p_igdb_id: int) -> list: + search_term: str = re.sub('[\(\[].*?[\)\]]', '', file_name.split('.')[0]) matched_roms: list = requests.post("https://api.igdb.com/v4/games/", headers=self.headers, data=f"search \"{search_term}\";fields name, id, slug, summary; where platforms=[{p_igdb_id}];").json() - log.info(f"Matched roms for {filename}: {matched_roms}") + log.info(f"Matched roms for {file_name}: {matched_roms}") for rom in matched_roms: try: res_details: dict = requests.post("https://api.igdb.com/v4/covers/", headers=self.headers, @@ -125,6 +124,8 @@ def get_matched_roms(self, filename: str, p_igdb_id: int) -> list: rom['url_cover'] = f"https:{res_details['url']}".replace('t_thumb', f't_cover_big') except IndexError: rom['url_cover'] = DEFAULT_URL_COVER_L + rom['r_igdb_id'] = rom.pop('id') + rom['r_slug'] = rom.pop('slug') return matched_roms diff --git a/backend/src/handler/sgdb_handler.py b/backend/src/handler/sgdb_handler.py index 647a9849b..1f5490a3e 100644 --- a/backend/src/handler/sgdb_handler.py +++ b/backend/src/handler/sgdb_handler.py @@ -1,6 +1,6 @@ import requests -from config.config import STEAMGRIDDB_API_KEY +from config import STEAMGRIDDB_API_KEY from logger.logger import log diff --git a/backend/src/main.py b/backend/src/main.py index 788e35d48..b270fa634 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -1,9 +1,10 @@ +import os from fastapi import FastAPI, Request import uvicorn from logger.logger import log from handler import igdbh, dbh -from config.config import DEV_PORT, DEV_HOST +from config import DEV_PORT, DEV_HOST from models.platform import Platform from utils import fs, fastapi @@ -12,47 +13,53 @@ fastapi.allow_cors(app) -@app.patch("/platforms/{p_slug}/roms/{filename}") -async def updateRom(req: Request, p_slug: str, filename: str) -> dict: +@app.get("/platforms/{p_slug}/roms/{file_name}") +async def rom(p_slug: str, file_name: str) -> dict: + """Returns one rom data of the desired platform""" + + return {'data': dbh.get_rom(p_slug, file_name)} + + +@app.patch("/platforms/{p_slug}/roms") +async def updateRom(req: Request, p_slug: str) -> dict: """Updates rom details""" data: dict = await req.json() - if 'r_igdb_id' in data: - r_igdb_id, filename_no_ext, r_slug, r_name, summary, url_cover = igdbh.get_rom_details(filename, data['p_igdb_id'], data['r_igdb_id']) - path_cover_s, path_cover_l, has_cover = fs.get_cover_details(True, p_slug, filename_no_ext, url_cover) - data['r_igdb_id'] = r_igdb_id - data['filename_no_ext'] = filename_no_ext - data['r_slug'] = r_slug - data['name'] = r_name - data['summary'] = summary - data['path_cover_s'] = path_cover_s - data['path_cover_l'] = path_cover_l - data['has_cover'] = has_cover - data['p_slug'] = p_slug - else: - fs.rename_rom(p_slug, filename, data) - data['filename_no_ext'] = data['filename'].split('.')[0] - dbh.update_rom(p_slug, filename, data) - return {'data': data} - - -@app.delete("/platforms/{p_slug}/roms/{filename}") -async def delete_rom(p_slug: str, filename: str, filesystem: bool=False) -> dict: + rom: dict = data['rom'] + updatedRom: dict = data['updatedRom'] + r_igdb_id, file_name_no_tags, r_slug, r_name, summary, url_cover = igdbh.get_rom_details(updatedRom['file_name'], rom['p_igdb_id'], updatedRom['r_igdb_id']) + path_cover_s, path_cover_l, has_cover = fs.get_cover_details(True, p_slug, updatedRom['file_name'], url_cover) + updatedRom['file_name_no_tags'] = file_name_no_tags + updatedRom['r_igdb_id'] = r_igdb_id + updatedRom['p_igdb_id'] = rom['p_igdb_id'] + updatedRom['r_slug'] = r_slug + updatedRom['p_slug'] = p_slug + updatedRom['name'] = r_name + updatedRom['summary'] = summary + updatedRom['path_cover_s'] = path_cover_s + updatedRom['path_cover_l'] = path_cover_l + updatedRom['has_cover'] = has_cover + updatedRom['file_path'] = rom['file_path'] + updatedRom['file_size'] = rom['file_size'] + updatedRom['file_extension'] = updatedRom['file_name'].split('.')[-1] if '.' in updatedRom['file_name'] else "" + reg, rev, other_tags = fs.parse_tags(updatedRom['file_name']) + updatedRom.update({'region': reg, 'revision': rev, 'tags': other_tags}) + if 'url_cover' in updatedRom.keys(): del updatedRom['url_cover'] + fs.rename_rom(p_slug, rom['file_name'], updatedRom['file_name']) + dbh.update_rom(p_slug, rom['file_name'], updatedRom) + return {'data': updatedRom} + + +@app.delete("/platforms/{p_slug}/roms") +async def delete_rom(p_slug: str, file_name: str, filesystem: bool=False) -> dict: """Detele rom from filesystem and database""" log.info("deleting rom...") - if filesystem: fs.delete_rom(p_slug, filename) - dbh.delete_rom(p_slug, filename) + if filesystem: fs.delete_rom(p_slug, file_name) + dbh.delete_rom(p_slug, file_name) return {'msg': 'success'} -@app.get("/platforms/{p_slug}/roms/{filename}") -async def rom(p_slug: str, filename: str) -> dict: - """Returns one rom data of the desired platform""" - - return {'data': dbh.get_rom(p_slug, filename)} - - @app.get("/platforms/{p_slug}/roms") async def roms(p_slug: str) -> dict: """Returns all roms of the desired platform""" @@ -90,8 +97,8 @@ async def search_rom_igdb(req: Request) -> dict: """Get all the roms matched from igdb.""" data: dict = await req.json() - log.info(f"getting {data['filename']} roms from {data['p_igdb_id']} igdb ...") - return {'data': igdbh.get_matched_roms(data['filename'], data['p_igdb_id'])} + log.info(f"getting {data['file_name']} roms from {data['p_igdb_id']} igdb ...") + return {'data': igdbh.get_matched_roms(data['file_name'], data['p_igdb_id'])} if __name__ == '__main__': diff --git a/backend/src/models/base.py b/backend/src/models/base.py index 91ea2247e..098b4e914 100644 --- a/backend/src/models/base.py +++ b/backend/src/models/base.py @@ -2,10 +2,9 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from config.config import get_db_engine +from config.config_loader import get_db_engine BaseModel = declarative_base() -# engine = create_engine(DB_DRIVERS[ROMM_DB_DRIVER], pool_pre_ping=True) engine = create_engine(get_db_engine(), pool_pre_ping=True) Session = sessionmaker(bind=engine, expire_on_commit=False) diff --git a/backend/src/models/platform.py b/backend/src/models/platform.py index afa2788de..753af2e4c 100644 --- a/backend/src/models/platform.py +++ b/backend/src/models/platform.py @@ -1,14 +1,17 @@ from sqlalchemy import Column, String, Integer, Text -from config.config import DEFAULT_PATH_LOGO +from config import DEFAULT_PATH_COVER_S from models.base import BaseModel class Platform(BaseModel): __tablename__ = 'platforms' - igdb_id = Column(String(length=50), default="") - sgdb_id = Column(String(length=50), default="") - slug = Column(String(length=500), primary_key=True) - name = Column(String(length=500), default="") - path_logo = Column(Text, default=DEFAULT_PATH_LOGO) + igdb_id = Column(String(length=10), default="") + sgdb_id = Column(String(length=10), default="") + + slug = Column(String(length=50), primary_key=True) + name = Column(String(length=400), default="") + + logo_path = Column(String(length=1000), default=DEFAULT_PATH_COVER_S) + n_roms = Column(Integer, default=0) diff --git a/backend/src/models/rom.py b/backend/src/models/rom.py index 4c013bbec..3be7cb30a 100644 --- a/backend/src/models/rom.py +++ b/backend/src/models/rom.py @@ -1,22 +1,33 @@ -from sqlalchemy import Column, String, Text, Boolean +from sqlalchemy import Column, String, Text, Boolean, Float, JSON -from config.config import DEFAULT_PATH_COVER_S, DEFAULT_PATH_COVER_L +from config import DEFAULT_PATH_COVER_S, DEFAULT_PATH_COVER_L from models.base import BaseModel class Rom(BaseModel): __tablename__ = 'roms' - filename = Column(String(length=500), primary_key=True) - filename_no_ext = Column(String(length=500), default="") - r_igdb_id = Column(String(length=50), default="") - p_igdb_id = Column(String(length=50), default="") - r_sgdb_id = Column(String(length=50), default="") - p_sgdb_id = Column(String(length=50), default="") - name = Column(String(length=500), default="") - r_slug = Column(String(length=500), default="") - p_slug = Column(String(length=500), default="") + r_igdb_id = Column(String(length=10), default="") + p_igdb_id = Column(String(length=10), default="") + r_sgdb_id = Column(String(length=10), default="") + p_sgdb_id = Column(String(length=10), default="") + + p_slug = Column(String(length=50), primary_key=True) + + file_name = Column(String(length=450), primary_key=True) + file_name_no_tags = Column(String(length=450), default="") + file_extension = Column(String(length=10), default="") + file_path = Column(String(length=1000), default="") + file_size = Column(Float, default="") + + name = Column(String(length=350), default="") + r_slug = Column(String(length=100), default="") + summary = Column(Text, default="") + path_cover_s = Column(Text, default=DEFAULT_PATH_COVER_S) path_cover_l = Column(Text, default=DEFAULT_PATH_COVER_L) has_cover = Column(Boolean, default=False) - size = Column(String(length=20), default="") + + region = Column(String(20), default="") + revision = Column(String(20), default="") + tags = Column(JSON, default=[]) diff --git a/backend/src/utils/fastapi.py b/backend/src/utils/fastapi.py index 1050069e2..43140ffa0 100644 --- a/backend/src/utils/fastapi.py +++ b/backend/src/utils/fastapi.py @@ -30,30 +30,27 @@ def scan_platform(p_slug: str) -> Platform: log.info(f"Getting {p_slug} details") platform_attrs: dict = igdbh.get_platform_details(p_slug) platform_attrs['slug'] = p_slug - platform_attrs['path_logo'] = '' + platform_attrs['logo_path'] = '' platform_attrs['n_roms'] = fs.get_roms(p_slug, only_amount=True) + log.info(f"Platform n_roms: {platform_attrs['n_roms']}") platform = Platform(**platform_attrs) dbh.add_platform(platform) return platform def scan_rom(platform: Platform, rom: dict, r_igbd_id_search: str = '', overwrite: bool = False) -> None: - log.info(f"Getting {rom['filename']} details") - r_igdb_id, filename_no_ext, r_slug, r_name, summary, url_cover = igdbh.get_rom_details(rom['filename'], platform.igdb_id, r_igbd_id_search) - path_cover_s, path_cover_l, has_cover = fs.get_cover_details(overwrite, platform.slug, filename_no_ext, url_cover) - rom_attrs: dict = { - 'filename': rom['filename'], - 'filename_no_ext': filename_no_ext, - 'size': rom['size'], - 'r_igdb_id': r_igdb_id, - 'p_igdb_id': platform.igdb_id, - 'name': r_name, - 'r_slug': r_slug, - 'p_slug': platform.slug, - 'summary': summary, - 'path_cover_s': path_cover_s, - 'path_cover_l': path_cover_l, - 'has_cover': has_cover - } - rom = Rom(**rom_attrs) + log.info(f"Getting {rom['file_name']} details") + r_igdb_id, file_name_no_tags, r_slug, r_name, summary, url_cover = igdbh.get_rom_details(rom['file_name'], platform.igdb_id, r_igbd_id_search) + path_cover_s, path_cover_l, has_cover = fs.get_cover_details(overwrite, platform.slug, rom['file_name'], url_cover) + rom['file_name_no_tags'] = file_name_no_tags + rom['r_igdb_id'] = r_igdb_id + rom['p_igdb_id'] = platform.igdb_id + rom['r_slug'] = r_slug + rom['p_slug'] = platform.slug + rom['name'] = r_name + rom['summary'] = summary + rom['path_cover_s'] = path_cover_s + rom['path_cover_l'] = path_cover_l + rom['has_cover'] = has_cover + rom = Rom(**rom) dbh.add_rom(rom) diff --git a/backend/src/utils/fs.py b/backend/src/utils/fs.py index c91717c03..b72cbc9a9 100644 --- a/backend/src/utils/fs.py +++ b/backend/src/utils/fs.py @@ -1,11 +1,12 @@ import os import shutil +import re from pathlib import Path import requests from fastapi import HTTPException -from config.config import LIBRARY_BASE_PATH, RESERVED_FOLDERS, DEFAULT_URL_LOGO, DEFAULT_URL_COVER_L, DEFAULT_PATH_COVER_L, DEFAULT_URL_COVER_S, DEFAULT_PATH_COVER_S +from config import user_config, LIBRARY_BASE_PATH, RESERVED_FOLDERS, DEFAULT_URL_COVER_L, DEFAULT_PATH_COVER_L, DEFAULT_URL_COVER_S, DEFAULT_PATH_COVER_S from logger.logger import log @@ -16,46 +17,13 @@ def store_default_resources(overwrite: bool) -> None: Args: overwrite: flag to overwrite or not default resources """ - if overwrite or not p_logo_exists('default'): - store_p_logo('default', DEFAULT_URL_LOGO) - if overwrite or not r_cover_exists('default', 'cover', 'l'): - store_r_cover('default', 'cover', DEFAULT_URL_COVER_L, 'l') - if overwrite or not r_cover_exists('default', 'cover', 's'): - store_r_cover('default', 'cover', DEFAULT_URL_COVER_S, 's') + if overwrite or not _cover_exists('default', 'cover', 'l'): + _store_cover('default', 'cover', DEFAULT_URL_COVER_L, 'l') + if overwrite or not _cover_exists('default', 'cover', 's'): + _store_cover('default', 'cover', DEFAULT_URL_COVER_S, 's') # ========= Platforms utils ========= -def p_logo_exists(slug: str) -> bool: - """Check if platform logo exists in filesystem - - Args: - slug: shor name of the platform - Returns - True if logo exists in filesystem else False - """ - logo_path: str = f"{LIBRARY_BASE_PATH}/resources/{slug}/logo.png" - return True if os.path.exists(logo_path) else False - - -def store_p_logo(slug: str, url_logo: str) -> None: - """Store platform resources in filesystem - - Args: - slug: shor name of the platform - url_logo: url to get logo - """ - logo_file: str = f"logo.png" - logo_path: str = f"{LIBRARY_BASE_PATH}/resources/{slug}" - res = requests.get(url_logo, stream=True) - if res.status_code == 200: - Path(logo_path).mkdir(parents=True, exist_ok=True) - with open(f"{logo_path}/{logo_file}", 'wb') as f: - shutil.copyfileobj(res.raw, f) - log.info(f"{slug} logo downloaded successfully!") - else: - log.warning(f"{slug} logo couldn't be downloaded") - - def get_platforms() -> list[str]: """Gets all filesystem platforms @@ -68,6 +36,14 @@ def get_platforms() -> list[str]: else: platforms: list[str] = list(os.walk(LIBRARY_BASE_PATH))[0][1] [platforms.remove(reserved) for reserved in RESERVED_FOLDERS if reserved in platforms] + try: + excluded_folders: list = user_config['exclude']['folders'] + try: + [platforms.remove(excluded) for excluded in excluded_folders if excluded in platforms] + except TypeError: + pass + except KeyError: + pass log.info(f"filesystem platforms found: {platforms}") return platforms except IndexError: @@ -75,41 +51,138 @@ def get_platforms() -> list[str]: # ========= Roms utils ========= -def r_cover_exists(p_slug: str, filename_no_ext: str, size: str) -> bool: +def _check_folder_structure(p_slug) -> tuple: + if os.path.exists(f"{LIBRARY_BASE_PATH}/roms"): + roms_path: str = f"{LIBRARY_BASE_PATH}/roms/{p_slug}" + roms_files = list(os.walk(f"{LIBRARY_BASE_PATH}/roms/{p_slug}"))[0][2] + else: + roms_path: str = f"{LIBRARY_BASE_PATH}/{p_slug}/roms" + roms_files = list(os.walk(f"{LIBRARY_BASE_PATH}/{p_slug}/roms"))[0][2] + return roms_path, roms_files + + +def _exclude_files(roms_files) -> list[str]: + try: + excluded_files: list = user_config['exclude']['files'] + filtered_files: list = [] + for file_name in roms_files: + if file_name.split('.')[-1] in excluded_files: + filtered_files.append(file_name) + roms_files = [f for f in roms_files if f not in filtered_files] + except (TypeError, KeyError): + pass + return roms_files + + +def parse_tags(file_name: str) -> tuple: + reg='' + rev='' + other_tags=[] + tags = re.findall('\(([^)]+)', file_name) + for t in tags: + if t.split('-')[0].lower() == 'reg': + try: reg=t.split('-', 1)[1] + except IndexError: pass + elif t.split('-')[0].lower() == 'rev': + try: rev=t.split('-', 1)[1] + except IndexError: pass + else: + other_tags.append(t) + return reg, rev, other_tags + + +def get_roms(p_slug: str, only_amount: bool = False) -> list[dict]: + """Gets all filesystem roms for a platform + + Args: + p_slug: short name of the platform + only_amount: flag to return only amount of roms instead of all info + Returns: list with all the filesystem roms for a platform found in the LIBRARY_BASE_PATH. Just the amount of them if only_amount=True + """ + try: + roms: list[dict] = [] + roms_path, roms_files = _check_folder_structure(p_slug) + roms_files = _exclude_files(roms_files) + + if only_amount: return len(roms_files) + + for rom in roms_files: + file_size: str = str(round(os.stat(f"{roms_path}/{rom}").st_size / (1024 * 1024), 2)) + file_extension: str = rom.split('.')[-1] if '.' in rom else "" + reg, rev, other_tags = parse_tags(rom) + roms.append({'file_name': rom, 'file_path': roms_path, 'file_size': file_size, 'file_extension': file_extension, + 'region': reg, 'revision': rev, 'tags': other_tags}) + log.info(f"Roms found for {p_slug}: {roms}") + except IndexError: + log.warning(f"Roms not found for {p_slug}") + if only_amount: return 0 + return roms + + +def _rom_exists(p_slug: str, file_name: str) -> bool: + """Check if rom exists in filesystem + + Args: + p_slug: short name of the platform + file_name: rom file_name + Returns + True if rom exists in filesystem else False + """ + rom_path, _ = _check_folder_structure(p_slug) + exists: bool = True if os.path.exists(f"{rom_path}/{file_name}") else False + return exists + + +def rename_rom(p_slug: str, old_name: str, new_name: str) -> None: + if new_name != old_name: + rom_path, _ = _check_folder_structure(p_slug) + if _rom_exists(p_slug, new_name): raise HTTPException(status_code=500, detail=f"Can't rename: {new_name} already exists.") + os.rename(f"{rom_path}/{old_name}", f"{rom_path}/{new_name}") + + +def delete_rom(p_slug: str, file_name: str) -> None: + try: + rom_path, _ = _check_folder_structure(p_slug) + os.remove(f"{rom_path}/{file_name}") + except FileNotFoundError: + log.warning(f"Rom not found in filesystem: {rom_path}/{file_name}") + + +def _cover_exists(p_slug: str, file_name: str, size: str) -> bool: """Check if rom cover exists in filesystem Args: p_slug: short name of the platform - filename_no_ext: name of rom file without extension + file_name: name of rom file size: size of the cover -> big as 'l' | small as 's' Returns True if cover exists in filesystem else False """ - logo_path: str = f"{LIBRARY_BASE_PATH}/resources/{p_slug}/{filename_no_ext}_{size}.png" + logo_path: str = f"{LIBRARY_BASE_PATH}/resources/{p_slug}/{file_name}_{size}.png" return True if os.path.exists(logo_path) else False -def get_r_cover_path(p_slug: str, filename_no_ext: str, size: str) -> str: +def _get_cover_path(p_slug: str, file_name: str, size: str) -> str: """Returns platform logo filesystem path Args: p_slug: short name of the platform - filename_no_ext: name of rom file without extension + file_name: name of rom file size: size of the cover -> big as 'l' | small as 's' """ - return f"/assets/library/resources/{p_slug}/{filename_no_ext}_{size}.png" + return f"/assets/library/resources/{p_slug}/{file_name}_{size}.png" -def store_r_cover(p_slug: str, filename_no_ext: str, url_cover: str, size: str) -> None: +def _store_cover(p_slug: str, file_name: str, url_cover: str, size: str) -> None: """Store roms resources in filesystem Args: p_slug: short name of the platform - filename_no_ext: name of rom file without extension + file_name: name of rom file url_cover: url to get the cover size: size of the cover -> big as 'l' | small as 's' """ - cover_file: str = f"{filename_no_ext}_{size}.png" + cover_file: str = f"{file_name}_{size}.png" cover_path: str = f"{LIBRARY_BASE_PATH}/resources/{p_slug}/" sizes: dict = {'l': 'big', 's': 'small'} res = requests.get(url_cover.replace('t_thumb', f't_cover_{sizes[size]}'), stream=True) @@ -117,75 +190,24 @@ def store_r_cover(p_slug: str, filename_no_ext: str, url_cover: str, size: str) Path(cover_path).mkdir(parents=True, exist_ok=True) with open(f"{cover_path}/{cover_file}", 'wb') as f: shutil.copyfileobj(res.raw, f) - log.info(f"{filename_no_ext} {sizes[size]} cover downloaded successfully!") + log.info(f"{file_name} {sizes[size]} cover downloaded successfully!") else: - log.warning(f"{filename_no_ext} {sizes[size]} cover couldn't be downloaded") + log.warning(f"{file_name} {sizes[size]} cover couldn't be downloaded") -def get_cover_details(overwrite: bool, p_slug: str, filename_no_ext: str, url_cover: str) -> tuple: +def get_cover_details(overwrite: bool, p_slug: str, file_name: str, url_cover: str) -> tuple: path_cover_s: str = DEFAULT_PATH_COVER_S path_cover_l: str = DEFAULT_PATH_COVER_L has_cover: int = 0 - if (overwrite or not r_cover_exists(p_slug, filename_no_ext, 's')) and url_cover: - store_r_cover(p_slug, filename_no_ext, url_cover, 's') - if r_cover_exists(p_slug, filename_no_ext, 's'): - path_cover_s = get_r_cover_path(p_slug, filename_no_ext, 's') + if (overwrite or not _cover_exists(p_slug, file_name, 's')) and url_cover: + _store_cover(p_slug, file_name, url_cover, 's') + if _cover_exists(p_slug, file_name, 's'): + path_cover_s = _get_cover_path(p_slug, file_name, 's') - if (overwrite or not r_cover_exists(p_slug, filename_no_ext, 'l')) and url_cover: - store_r_cover(p_slug, filename_no_ext, url_cover, 'l') - if r_cover_exists(p_slug, filename_no_ext, 'l'): - path_cover_l = get_r_cover_path(p_slug, filename_no_ext, 'l') + if (overwrite or not _cover_exists(p_slug, file_name, 'l')) and url_cover: + _store_cover(p_slug, file_name, url_cover, 'l') + if _cover_exists(p_slug, file_name, 'l'): + path_cover_l = _get_cover_path(p_slug, file_name, 'l') has_cover = 1 return path_cover_s, path_cover_l, has_cover - -def get_roms(p_slug: str, only_amount: bool = False) -> list[dict]: - """Gets all filesystem roms for a platform - - Args: - p_slug: short name of the platform - only_amount: flag to return only amount of roms instead of all info - Returns: list with all the filesystem roms for a platform found in the LIBRARY_BASE_PATH. Just the amount of them if only_amount=True - """ - try: - roms: list[dict] = [] - if os.path.exists(f"{LIBRARY_BASE_PATH}/roms"): - roms_path: str = f"{LIBRARY_BASE_PATH}/roms/{p_slug}" - roms_filename = list(os.walk(f"{LIBRARY_BASE_PATH}/roms/{p_slug}"))[0][2] - else: - roms_path: str = f"{LIBRARY_BASE_PATH}/{p_slug}/roms" - roms_filename = list(os.walk(f"{LIBRARY_BASE_PATH}/{p_slug}/roms"))[0][2] - if only_amount: return len(roms_filename) - [roms.append({'filename': rom, 'size': str(round(os.stat(f"{roms_path}/{rom}").st_size / (1024 * 1024), 2))}) for rom in roms_filename] - log.info(f"filesystem roms found for {p_slug}: {roms}") - except IndexError: - log.warning(f"roms not found for {p_slug}") - return roms - - -def r_exists(p_slug: str, filename: str) -> bool: - """Check if rom exists in filesystem - - Args: - p_slug: short name of the platform - filename: rom filename - Returns - True if rom exists in filesystem else False - """ - rom_path: str = f"{LIBRARY_BASE_PATH}/{p_slug}/roms/{filename}" - exists: bool = True if os.path.exists(rom_path) else False - return exists - - -def rename_rom(p_slug: str, filename: str, data: dict) -> None: - if data['filename'] != filename: - if r_exists(p_slug, data['filename']): raise HTTPException(status_code=500, detail=f"Can't rename: {data['filename']} already exists.") - os.rename(f"{LIBRARY_BASE_PATH}/{p_slug}/roms/{filename}", - f"{LIBRARY_BASE_PATH}/{p_slug}/roms/{data['filename']}") - - -def delete_rom(p_slug: str, filename: str) -> None: - try: - os.remove(f"{LIBRARY_BASE_PATH}/{p_slug}/roms/{filename}") - except FileNotFoundError: - log.warning(f"Rom not found in filesystem: {p_slug}/{filename}") diff --git a/changelog.md b/changelog.md index 6018b2ace..7487083ce 100644 --- a/changelog.md +++ b/changelog.md @@ -1,41 +1,17 @@ -# v1.5 (_29-03-2023_) +# v1.5 (_30-03-2023_) + +**`Breaking change`** + +In order to make the new features structure to work, it is mandatory this time to drop all the database. This will only make you need to re-scan, but you won't lose the cover changes or file changes you made. + +I apologize for the inconveniences this may cause, as this is a new software, it may change a little bit the first weeks, at least until I can develop a proper way to migrate between versions. I hope you can understand these initial wipes in order to make a better tool. ## Added - - Now RomM folder structure is more flexible to match two different patrons by priority: - - Structure 1 (priority high) - roms folder at root of library folder: - ``` - library/ - ├─ roms/ - │ ├─ gbc/ - │ ├─ rom_1.gbc - │ ├─ rom_2.gbc - │ - ├─ gba/ - │ ├─ rom_1.gba - │ ├─ rom_2.gba - │ - ├─ gb/ - ├─ rom_1.gb - ├─ rom_1.gb - ``` - - Structure 2 (priority low) - roms folder inside each platform folder - ``` - library/ - ├─ gbc/ - │ ├─ roms/ - │ ├─ rom_1.gbc - │ ├─ rom_2.gbc - | - ├─ gba/ - │ ├─ roms/ - │ ├─ rom_1.gba - │ ├─ rom_2.gba - | - ├─ gb/ - │ ├─ roms/ - │ ├─ rom_1.gb - │ ├─ rom_1.gb - ``` + - Now RomM folder structure is more flexible to match two different patrons by priority. This change makes RomM Emudeck compatible at least with single file games platforms. Check [folder structure](readme.md#⚠️-folder-structure) + + - Added config file support to exclude folders and specific extension files to be scanned. To reload config file RomM reload is needed. Check [config](readme.md#configuration). + + - Added tags support for region, revision/version and generic tags. Tags must have the right prefix to allow RomM scan them properly. Check [tags](readme.md#📑-tags-support). # v1.4.1 (_29-03-2023_) @@ -46,6 +22,7 @@ ## Added - Gamecube support [platforms support](https://github.com/zurdi15/romm#platforms-support) + - PC support added (only for single file games like zip, iso, etc) [platforms support](https://github.com/zurdi15/romm#platforms-support) ## Changed @@ -56,23 +33,25 @@ ## Fixed **`Breaking change`** - **This breaking change only applies for mariaDB users**: -Some users reported errors when scanning files with large names because filenames are limited to 100 characters in the database. As I want to give as much flexibility as possible I changed some database columns. +Some users reported errors when scanning files with large names because file_names are limited to 100 characters in the database. As I want to give as much flexibility as possible I changed some database columns. If you didn't make a lot of manual changes you can just get rid of the database and recreate it, scanning your library again. If you did some changes and don't want to lose the progress, you should do this changes manually from the mariadb container (or wherever you have your mariadb database) since there is not any kind of CLI for this migration. I am so sorry for any inconvenience this can generate. Columns to modify (examples in case that you set it with database name as romm, in other case just change the database name in the {db_name}.roms part): - - alter table romm.roms modify column filename varchar(500); - - alter table romm.roms modify column filename_no_ext varchar(500); - - alter table romm.roms modify column name varchar(500); - - alter table romm.roms modify column r_slug varchar(500); - - alter table romm.roms modify column p_slug varchar(500); - - alter table romm.roms modify column path_cover_l text; - - alter table romm.roms modify column path_cover_s text; - - alter table romm.platforms modify column slug varchar(500); - - alter table romm.platforms modify column name varchar(500); - - alter table romm.platforms modify column path_logo text; +``` + alter table romm.roms modify column file_name varchar(500); + alter table romm.roms modify column file_name_no_ext varchar(500); + alter table romm.roms modify column name varchar(500); + alter table romm.roms modify column r_slug varchar(500); + alter table romm.roms modify column p_slug varchar(500); + alter table romm.roms modify column path_cover_l text; + alter table romm.roms modify column path_cover_s text; + alter table romm.platforms modify column slug varchar(500); + alter table romm.platforms modify column name varchar(500); + alter table romm.platforms modify column path_logo text; +``` # v1.2.2 (_28-03-2023_) diff --git a/docker/docker-compose.example.yml b/docker/docker-compose.example.yml index d034b163e..51d4fd8a4 100644 --- a/docker/docker-compose.example.yml +++ b/docker/docker-compose.example.yml @@ -15,6 +15,7 @@ services: - STEAMGRIDDB_API_KEY=WIP volumes: - '/path/to/library:/library' + - '/path/to/config.yml:/romm/config.yml' ports: - '80:80' depends_on: diff --git a/docker/init_scripts/init_back b/docker/init_scripts/init_back index 492db50df..0f0918fba 100755 --- a/docker/init_scripts/init_back +++ b/docker/init_scripts/init_back @@ -1,4 +1,4 @@ #!/bin/bash cd /back -uvicorn main:app --proxy-headers --host 0.0.0.0 --port 5000 --workers 4 \ No newline at end of file +uvicorn main:app --proxy-headers --host 0.0.0.0 --port 5000 --workers 2 \ No newline at end of file diff --git a/frontend/src/components/RomDetails.vue b/frontend/src/components/RomDetails.vue index d7d4d68b9..3ad0964bf 100644 --- a/frontend/src/components/RomDetails.vue +++ b/frontend/src/components/RomDetails.vue @@ -10,7 +10,7 @@ const saveFiles = ref(false) const searching = ref(false) const matchedRoms = ref([]) const updating = ref(false) -const romNewName = ref(rom.value.filename) +const editedRomName = ref(rom.value.file_name) const dialogSearchRom = ref(false) const dialogEditRom = ref(false) const dialogDeleteRom = ref(false) @@ -23,21 +23,21 @@ emitter.on('currentRom', (currentRom) => { rom.value = currentRom }) // Functions function downloadingRom(rom) { - emitter.emit('snackbarScan', {'msg': "Downloading "+rom.filename, 'icon': 'mdi-download', 'color': 'green'}) + emitter.emit('snackbarScan', {'msg': "Downloading "+rom.file_name, 'icon': 'mdi-download', 'color': 'green'}) downloadRom(rom) } function downloadingSave() { - // emitter.emit('snackbarScan', {'msg': "Downloading "+rom.filename+" savefile", 'icon': 'mdi-download', 'color': 'green'}) + emitter.emit('snackbarScan', {'msg': "Downloading "+rom.file_name+" savefile", 'icon': 'mdi-download', 'color': 'green'}) downloadSave() } async function searchRomIGDB() { searching.value = true dialogSearchRom.value = true - console.log("searching for rom... "+rom.value.filename) + console.log("searching for rom... "+rom.value.file_name) await axios.put('/api/search/roms/igdb', { - filename: rom.value.filename, + file_name: rom.value.file_name, p_igdb_id: rom.value.p_igdb_id }).then((response) => { console.log(response.data.data) @@ -46,53 +46,37 @@ async function searchRomIGDB() { searching.value = false } -async function updateRom(newRomRaw) { +async function updateRom(updatedRom=Object.assign({},rom.value), newName=rom.value.file_name) { updating.value = true dialogSearchRom.value = false - const newRom = toRaw(newRomRaw) - newRom.filename = rom.value.filename - console.log(newRom) - await axios.patch('/api/platforms/'+rom.value.p_slug+'/roms/'+rom.value.filename, { - filename: rom.value.filename, - r_igdb_id: newRom.id, - p_igdb_id: rom.value.p_igdb_id + updatedRom.file_name = newName + console.log(rom.value) + await axios.patch('/api/platforms/'+rom.value.p_slug+'/roms', { + rom: rom.value, + updatedRom: updatedRom }).then((response) => { - console.log("update "+rom.value.filename+" completed") + console.log('updated rom: '+JSON.stringify(rom.value)) localStorage.setItem('currentRom', JSON.stringify(response.data.data)) - emitter.emit('snackbarScan', {'msg': rom.value.filename+" updated successfully!", 'icon': 'mdi-check-bold', 'color': 'green'}) + emitter.emit('snackbarScan', {'msg': rom.value.file_name+" updated successfully!", 'icon': 'mdi-check-bold', 'color': 'green'}) rom.value = response.data.data }).catch((error) => { console.log(error) - emitter.emit('snackbarScan', {'msg': "Couldn't updated "+rom.value.filename+". Something went wrong...", 'icon': 'mdi-close-circle', 'color': 'red'}) + emitter.emit('snackbarScan', {'msg': "Couldn't updated "+rom.value.file_name+". Something went wrong...", 'icon': 'mdi-close-circle', 'color': 'red'}) }) updating.value = false -} - -async function editRom() { - await axios.patch('/api/platforms/'+rom.value.p_slug+'/roms/'+rom.value.filename, { - filename: romNewName.value - }).then((response) => { - console.log(response) - console.log("update "+rom.value.filename+" to "+romNewName.value) - rom.value.filename = romNewName.value - emitter.emit('snackbarScan', {'msg': romNewName.value+" edited successfully!", 'icon': 'mdi-check-bold', 'color': 'green'}) - dialogEditRom.value = false - }).catch((error) => { - console.log(error) - emitter.emit('snackbarScan', {'msg': error.response.data.detail, 'icon': 'mdi-close-circle', 'color': 'red'}) - }) + dialogEditRom.value = false } async function deleteRom() { - console.log('deleting rom '+ rom.value.filename) - await axios.delete('/api/platforms/'+rom.value.p_slug+'/roms/'+rom.value.filename+'?filesystem='+deleteFromFs.value) + console.log('deleting rom '+ rom.value.file_name) + await axios.delete('/api/platforms/'+rom.value.p_slug+'/roms/'+rom.value.file_name+'?filesystem='+deleteFromFs.value) .then((response) => { console.log(response) - emitter.emit('snackbarScan', {'msg': rom.value.filename+" deleted successfully!", 'icon': 'mdi-check-bold', 'color': 'green'}) + emitter.emit('snackbarScan', {'msg': rom.value.file_name+" deleted successfully!", 'icon': 'mdi-check-bold', 'color': 'green'}) router.push(import.meta.env.BASE_URL) }).catch((error) => { console.log(error) - emitter.emit('snackbarScan', {'msg': "Couldn't delete "+rom.value.filename+". Something went wrong...", 'icon': 'mdi-close-circle', 'color': 'red'}) + emitter.emit('snackbarScan', {'msg': "Couldn't delete "+rom.value.file_name+". Something went wrong...", 'icon': 'mdi-close-circle', 'color': 'red'}) }) } @@ -153,38 +137,18 @@ async function deleteRom() { - - IGDB id - {{ rom.r_igdb_id }} - - - Name - {{ rom.name }} - - - File - {{ rom.filename }} - - - Size - {{ rom.size }} MB - - - Slug - {{ rom.r_slug }} - - - Platform - {{ rom.p_slug }} - - - Cover - {{ rom.path_cover_l }} - - - Summary - {{ rom.summary }} - + IGDB id{{ rom.r_igdb_id }} + Name{{ rom.name }} + File{{ rom.file_name }} + Path{{ rom.file_path }} + Region{{ rom.region }} + Revision{{ rom.revision }} + Tags{{ tag }} + Size{{ rom.file_size }} MB + Slug{{ rom.r_slug }} + Platform{{ rom.p_slug }} + Cover{{ rom.path_cover_l }} + Summary{{ rom.summary }} @@ -208,7 +172,7 @@ async function deleteRom() {

No results found

- + {{ rom.name }} @@ -228,14 +192,14 @@ async function deleteRom() { - Editing {{ rom.filename }} + Editing {{ rom.file_name }} mdi-close - - - Apply + + + Apply @@ -244,7 +208,7 @@ async function deleteRom() { - Deleting {{ rom.filename }} + Deleting {{ rom.file_name }} mdi-close diff --git a/frontend/src/components/RomsGallery.vue b/frontend/src/components/RomsGallery.vue index 15ee9e0ba..5a7212290 100644 --- a/frontend/src/components/RomsGallery.vue +++ b/frontend/src/components/RomsGallery.vue @@ -50,16 +50,16 @@ function normalizeString(s) { function setFilter(filter) { currentFilter.value = normalizeString(filter) romsFiltered.value = roms.value.filter(rom => { - return normalizeString(rom.filename).includes(currentFilter.value) + return normalizeString(rom.file_name).includes(currentFilter.value) }) } function downloadingRom(rom) { - emitter.emit('snackbarScan', {'msg': "Downloading "+rom.filename, 'icon': 'mdi-download', 'color': 'green'}) + emitter.emit('snackbarScan', {'msg': "Downloading "+rom.file_name, 'icon': 'mdi-download', 'color': 'green'}) downloadRom(rom) } function downloadingSave() { - // emitter.emit('snackbarScan', {'msg': "Downloading "+rom.filename+" savefile", 'icon': 'mdi-download', 'color': 'green'}) + // emitter.emit('snackbarScan', {'msg': "Downloading "+rom.file_name+" savefile", 'icon': 'mdi-download', 'color': 'green'}) downloadSave() } @@ -73,7 +73,7 @@ onMounted(() => { if(localStorage.getItem('currentPlatform')){ getRoms(JSON.pars - +
- {{ rom.filename }} + {{ rom.file_name }}
- +
+ {{ rom.region }} + {{ rom.revision }} +
- - + + + + +
@@ -122,6 +128,9 @@ onMounted(() => { if(localStorage.getItem('currentPlatform')){ getRoms(JSON.pars opacity: 1; } .v-card:not(.on-hover) { - opacity: 0.95; + opacity: 0.85; +} +.cover{ + cursor: pointer; } \ No newline at end of file diff --git a/frontend/src/plugins/vuetify.js b/frontend/src/plugins/vuetify.js index 0e4f7232b..9dee5bc11 100644 --- a/frontend/src/plugins/vuetify.js +++ b/frontend/src/plugins/vuetify.js @@ -26,7 +26,9 @@ export default createVuetify({ primary: '#FFFFFF', secondary: '#BDBDBD', toolbar: '#FFFFFF', - background: '#FFFFFF' + background: '#FFFFFF', + tag: '#FFFFFF', + tagBg: '#212121' } }, dark: { @@ -34,7 +36,9 @@ export default createVuetify({ primary: '#212121', secondary: '#424242', toolbar: '#212121', - background: '#212121' + background: '#212121', + tag: '#FFFFFF', + tagBg: '#212121' } } } diff --git a/frontend/src/utils/utils.js b/frontend/src/utils/utils.js index b735ed144..8def2cab4 100644 --- a/frontend/src/utils/utils.js +++ b/frontend/src/utils/utils.js @@ -3,12 +3,12 @@ import { saveAs } from 'file-saver' export function downloadRom(rom) { - console.log("Downloading "+rom.filename) - axios.get('/assets/library/'+rom.p_slug+'/roms/'+rom.filename, { responseType: 'blob' }).then(response => { - saveAs(new Blob([response.data], { type: 'application/file' }), rom.filename) + console.log("Downloading "+rom.file_name) + axios.get('/assets/library/'+rom.p_slug+'/roms/'+rom.file_name, { responseType: 'blob' }).then(response => { + saveAs(new Blob([response.data], { type: 'application/file' }), rom.file_name) }).catch((error) => {console.log(error)}) } export function downloadSave(rom) { - console.log("Downloading "+rom.filename+" save file") + console.log("Downloading "+rom.file_name+" save file") } \ No newline at end of file diff --git a/readme.md b/readme.md index acf126929..a278cfde9 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,6 @@
Gameyfin Logo

RomM (Rom Manager)

-

# Overview @@ -17,6 +16,7 @@ For now, it is only available as a docker [image](https://hub.docker.com/r/zurdi * Possibility to select one of the matching IGDB results if the scan doesn't get the right one * Download games directly from your web-browser * Edit your game files directly from your web-browser +* Region, revision/version and extra tags support * Works with SQLite or MaridDB (SQLite by default) * Responsive design * Light and dark theme @@ -33,7 +33,10 @@ For now, it is only available as a docker [image](https://hub.docker.com/r/zurdi ## ⚠️ Folder structure -RomM accepts two different folder structure by priority. RomM will try to find the structure 1. If it doesn't exists, RomM will try to find structure 2 +RomM accepts two different folder structure by priority. + +RomM will try to find the structure 1 and if it doesn't exists, RomM will try to find structure 2. + - Structure 1 (priority high) - roms folder at root of library folder: ``` library/ @@ -79,25 +82,43 @@ https://user-images.githubusercontent.com/34356590/227992371-33056130-c067-49c1- https://user-images.githubusercontent.com/34356590/228007442-0a9cbf6b-4b62-4c1a-aad8-48b13e6337e8.mp4 -# Docker image +# Installation -Last version of the docker [image](https://hub.docker.com/r/zurdi15/romm/tags) +## 🐳 Docker -## 🐳 Installation +Last version of the docker [image](https://hub.docker.com/r/zurdi15/romm/tags) Check the [docker-compose.yml](https://github.com/zurdi15/romm/blob/master/docker/docker-compose.example.yml) example Get API key from [IGDB](https://api-docs.igdb.com/#about) for the CLIENT_ID and CLIENT_SECRET variables. -# Platforms support +# Configuration + +## ⚙️ Config yml file + +RomM can be configured through a yml file. This is used to exclude folders and files with a certain extension to be scanned. For a configuration change to take effect, RomM must be restarted. + +Config file example: -## 🎮 Naming convention +``` +exclude: + folders: + - 'folder_1_to_exclude' + - 'folder_2_to_exclude' + files: + - 'txt' + - 'file_extension_to_exclude' +``` -If the RomM folders structure is followed, any kind of platform/folder-name is supported for the core features. For having extra metadata as well as cover images and platforms icons, the following table shows how to name your platforms folders. +# Naming convention + +## 🎮 Platforms support + +If the RomM [folder structure](#⚠️-folder-structure) is followed, any kind of platform/folder-name is supported for the core features. For having extra metadata as well as cover images and platforms icons, the following table shows how to name your platforms folders. This will change over the time, adding games metadata for more platforms. Make sure that the platforms folder names are lowercase. | slug | name | games metadata | -|---------------|-------------------------------------|----------------| +|---------------|-------------------------------------| :----: | | 3ds | Nintendo 3DS | ✅ | | amiga | Amiga | ✅ | | arcade | Arcade | ✅ | @@ -153,14 +174,28 @@ This will change over the time, adding games metadata for more platforms. Make s | wii | Wii | ✅ | | win | PC (Microsoft Windows) | ✅ | -## ⛏ Troubleshoot +## 📑 Tags support + +Games can be tagged with region, revision or other tags using parenthesis in the file name. Region and revision tags must be built with the following reserved words: + - Region tags must be prefixed with **"reg-"**: (reg-EUR) / (reg-USA) / (reg-Japan) / (reg-whatever) + - Revision tags must be prefixed with **"rev-"**: (rev-1) / (rev-v2) / (rev-whatever) + - Any other tag can have any structure + - Example: **my_game (reg-EUR)(rev-1)(aditional_tag_1)(aditional_tag_2).gba** + +Tags can be used with the search bar to help to filter your library. + +# ⛏ Troubleshoot * After the first installation, sometimes the RomM container can have problems connecting with the database. Restarting the RomM container may solve the problem. -## 🧾 References +# 🧾 References * Complete [changelog](https://github.com/zurdi15/romm/blob/master/CHANGELOG.md) -## 🎖 Credits +# 🎖 Credits * Pc icon support - Keyboard and mouse icons created by Flat Icons - Flaticon + +# ❤️ Support RomM + +Buy Me A Coffee \ No newline at end of file