diff --git a/backend/alembic/versions/0102_publisher_developer_split.py b/backend/alembic/versions/0102_publisher_developer_split.py new file mode 100644 index 0000000000..72afea516b --- /dev/null +++ b/backend/alembic/versions/0102_publisher_developer_split.py @@ -0,0 +1,397 @@ +"""Split company metadata into distinct publishers and developers + +``companies`` amalgamates the publisher and the developer of a game into one +list at ingestion, so the distinction (which providers like ScreenScraper, +RetroAchievements, LaunchBox, Flashpoint, gamelist and IGDB do expose) was lost +and could not be round-tripped to external tools. See issue #3518. + +The handlers now also emit ``publishers`` and ``developers`` alongside the +existing (unchanged) ``companies`` list. This migration threads the two new +fields through the read pipeline that 0098/0100/0101 built: + +- Two STORED generated columns on ``roms`` (``generated_publishers`` / + ``generated_developers``), coalescing ``$.publishers`` / ``$.developers`` + across the same provider blobs ``generated_companies`` already reads. +- The ``roms_metadata`` view projects them (appended, so nothing else shifts). +- ``roms_facets`` mirrors them; its sync triggers are rebuilt to carry them. +- ``virtual_collection_roms`` gains ``publisher`` / ``developer`` membership + types (backfilled once here, then kept current by the rebuilt triggers). The + ``virtual_collections`` view groups by ``(type, name)`` generically, so it + surfaces the new types with no change of its own. + +Existing rows only carry ``companies`` until their ROMs are re-scanned: the raw +provider blobs predate the new keys, so the generated columns read empty for +them. ``companies`` stays fully populated meanwhile. + +Revision ID: 0102_publisher_developer_split +Revises: 0101_virtual_collection_roms +Create Date: 2026-07-23 00:00:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op # type: ignore[attr-defined] + +from utils.database import CustomJSON, is_postgresql + +# revision identifiers, used by Alembic. +revision = "0102_publisher_developer_split" +down_revision = "0101_virtual_collection_roms" +branch_labels = None +depends_on = None + + +# --------------------------------------------------------------------------- +# Generated columns on roms (mirrors 0098's company source precedence/exprs) +# --------------------------------------------------------------------------- + +# (generated column, JSON key) added by this migration. +_NEW_GENERATED = [ + ("generated_publishers", "publishers"), + ("generated_developers", "developers"), +] + +# Same provider blobs (and precedence) that back generated_companies in 0098. +_COMPANY_SOURCES = [ + "manual_metadata", + "igdb_metadata", + "ss_metadata", + "ra_metadata", + "launchbox_metadata", + "flashpoint_metadata", + "gamelist_metadata", +] + + +def _maria_array_expr(key: str) -> str: + branches = [ + f"CASE WHEN JSON_LENGTH(JSON_EXTRACT({src}, '$.{key}')) > 0 " + f"THEN JSON_EXTRACT({src}, '$.{key}') ELSE NULL END" + for src in _COMPANY_SOURCES + ] + branches.append("JSON_ARRAY()") + return "COALESCE(\n " + ",\n ".join(branches) + "\n)" + + +def _postgres_array_expr(key: str) -> str: + branches = [f"NULLIF({src} -> '{key}', '[]'::jsonb)" for src in _COMPANY_SOURCES] + branches.append("'[]'::jsonb") + return "COALESCE(\n " + ",\n ".join(branches) + "\n)" + + +def _add_generated_columns(pg: bool) -> None: + json_type = "JSONB" if pg else "JSON" + expr = _postgres_array_expr if pg else _maria_array_expr + adds = ",\n".join( + f"ADD COLUMN {name} {json_type} GENERATED ALWAYS AS ({expr(key)}) STORED" + for name, key in _NEW_GENERATED + ) + op.execute(f"ALTER TABLE roms\n{adds}") # nosec B608 + + +def _drop_generated_columns() -> None: + drops = ",\n".join(f"DROP COLUMN {name}" for name, _ in _NEW_GENERATED) + op.execute(f"ALTER TABLE roms\n{drops}") # nosec B608 + + +# --------------------------------------------------------------------------- +# roms_metadata view (verbatim 0098 projection; publishers/developers appended) +# --------------------------------------------------------------------------- + +# 0098 column order, kept intact so the appended columns don't shift the rest. +_BASE_VIEW_COLUMNS = [ + ("generated_genres", "genres"), + ("generated_franchises", "franchises"), + ("generated_collections", "collections"), + ("generated_companies", "companies"), + ("generated_game_modes", "game_modes"), + ("generated_age_ratings", "age_ratings"), + ("generated_first_release_date", "first_release_date"), + ("generated_average_rating", "average_rating"), + ("generated_player_count", "player_count"), +] +_NEW_VIEW_COLUMNS = [ + ("generated_publishers", "publishers"), + ("generated_developers", "developers"), +] + + +def _roms_metadata_view_sql(pg: bool, include_new: bool) -> str: + columns = _BASE_VIEW_COLUMNS + (_NEW_VIEW_COLUMNS if include_new else []) + projections = [] + for name, alias in columns: + # player_count is text in the view (0098 note), cast to match on PG. + if pg and alias == "player_count": + projections.append(f"{name}::text AS {alias}") + else: + projections.append(f"{name} AS {alias}") + projection = ",\n ".join(projections) + return ( + "CREATE VIEW roms_metadata AS\n" # nosec B608 + "SELECT\n" + " id AS rom_id,\n" + " NOW() AS created_at,\n" + " NOW() AS updated_at,\n" + f" {projection}\n" + "FROM roms" + ) + + +def _rebuild_roms_metadata_view(pg: bool, include_new: bool) -> None: + # DROP + CREATE (not REPLACE): PG's CREATE OR REPLACE VIEW cannot drop the + # trailing columns on downgrade. Nothing depends on this view. + op.execute("DROP VIEW IF EXISTS roms_metadata") + op.execute(_roms_metadata_view_sql(pg, include_new)) + + +# --------------------------------------------------------------------------- +# roms_facets mirror (mirrors 0100; publishers/developers added) +# --------------------------------------------------------------------------- + +_FACETS_BASE = [ + ("platform_id", "platform_id"), + ("genres", "generated_genres"), + ("franchises", "generated_franchises"), + ("collections", "generated_collections"), + ("companies", "generated_companies"), + ("game_modes", "generated_game_modes"), + ("age_ratings", "generated_age_ratings"), + ("player_count", "generated_player_count"), + ("regions", "regions"), + ("languages", "languages"), + ("tags", "tags"), +] +_FACETS_NEW = [ + ("publishers", "generated_publishers"), + ("developers", "generated_developers"), +] + +_MYSQL_FACETS_TRIGGERS = { + "roms_facets_after_insert": "AFTER INSERT", + "roms_facets_after_update": "AFTER UPDATE", +} + + +def _facets_columns(include_new: bool) -> list[tuple[str, str]]: + # publishers/developers slot right after companies to keep the table tidy. + if not include_new: + return _FACETS_BASE + out: list[tuple[str, str]] = [] + for target, source in _FACETS_BASE: + out.append((target, source)) + if target == "companies": + out.extend(_FACETS_NEW) + return out + + +def _rebuild_facets_triggers(pg: bool, include_new: bool) -> None: + columns = _facets_columns(include_new) + targets = ", ".join(target for target, _ in columns) + if pg: + op.execute("DROP TRIGGER IF EXISTS roms_facets_sync ON roms") + op.execute("DROP FUNCTION IF EXISTS romm_sync_rom_facets()") + values = ", ".join(f"NEW.{source}" for _, source in columns) + assignments = ", ".join( + f"{target} = EXCLUDED.{target}" for target, _ in columns + ) + op.execute(f""" +CREATE OR REPLACE FUNCTION romm_sync_rom_facets() RETURNS trigger + LANGUAGE plpgsql AS $$ +BEGIN + INSERT INTO roms_facets (rom_id, {targets}) + VALUES (NEW.id, {values}) + ON CONFLICT (rom_id) DO UPDATE SET + {assignments}, + updated_at = NOW(); + RETURN NULL; +END $$ +""") # nosec B608 + op.execute( + "CREATE TRIGGER roms_facets_sync AFTER INSERT OR UPDATE ON roms\n" + " FOR EACH ROW EXECUTE FUNCTION romm_sync_rom_facets()" + ) + else: + for name in _MYSQL_FACETS_TRIGGERS: + op.execute(f"DROP TRIGGER IF EXISTS {name}") + values = ", ".join(f"NEW.{source}" for _, source in columns) + updates = ",\n".join(f"{target} = VALUES({target})" for target, _ in columns) + body = ( + f"INSERT INTO roms_facets (rom_id, {targets})\n" # nosec B608 + f"VALUES (NEW.id, {values})\n" + f"ON DUPLICATE KEY UPDATE\n{updates},\nupdated_at = CURRENT_TIMESTAMP" + ) + for name, timing in _MYSQL_FACETS_TRIGGERS.items(): + op.execute(f"CREATE TRIGGER {name} {timing} ON roms\nFOR EACH ROW\n{body}") + + +def _backfill_facets_new_columns(pg: bool) -> None: + if pg: + op.execute( + "UPDATE roms_facets f SET\n" + " publishers = r.generated_publishers,\n" + " developers = r.generated_developers\n" + "FROM roms r WHERE r.id = f.rom_id" + ) + else: + op.execute( + "UPDATE roms_facets f JOIN roms r ON r.id = f.rom_id SET\n" + " f.publishers = r.generated_publishers,\n" + " f.developers = r.generated_developers" + ) + + +# --------------------------------------------------------------------------- +# virtual_collection_roms membership (mirrors 0101; publisher/developer added) +# --------------------------------------------------------------------------- + +TABLE = "virtual_collection_roms" +NAME_MAX_LENGTH = 400 +_VC_COLUMNS = "rom_id, type, name, path_cover_s, path_cover_l, created_at, updated_at" + +_VC_BASE_TYPES = [ + ("genre", "generated_genres"), + ("franchise", "generated_franchises"), + ("collection", "generated_collections"), + ("mode", "generated_game_modes"), + ("company", "generated_companies"), +] +_VC_NEW_TYPES = [ + ("publisher", "generated_publishers"), + ("developer", "generated_developers"), +] +_VC_ALL_TYPES = _VC_BASE_TYPES + _VC_NEW_TYPES + + +def _mysql_vc_rows(row: str, from_roms: bool, types: list[tuple[str, str]]) -> str: + branches = [] + for type_, column in types: + json_table = ( + f"JSON_TABLE({row}.{column}, '$[*]' COLUMNS (value TEXT PATH '$')) j" + ) + source = f"roms r CROSS JOIN {json_table}" if from_roms else json_table + branches.append( + f"SELECT DISTINCT {row}.id, '{type_}', LEFT(j.value, {NAME_MAX_LENGTH}), " # nosec B608 + f"{row}.path_cover_s, {row}.path_cover_l, NOW(), NOW()\n" + f"FROM {source}\n" + f"WHERE j.value IS NOT NULL AND j.value != ''" + ) + return "\nUNION ALL\n".join(branches) + + +def _postgres_vc_rows(row: str, from_roms: bool, types: list[tuple[str, str]]) -> str: + branches = [] + for type_, column in types: + array = ( + f"CASE WHEN jsonb_typeof({row}.{column}) = 'array' " + f"THEN {row}.{column} ELSE '[]'::jsonb END" + ) + elements = f"jsonb_array_elements_text({array}) AS j(value)" + source = f"roms r CROSS JOIN LATERAL {elements}" if from_roms else elements + branches.append( + f"SELECT DISTINCT {row}.id, '{type_}', LEFT(j.value, {NAME_MAX_LENGTH}), " # nosec B608 + f"{row}.path_cover_s, {row}.path_cover_l, NOW(), NOW()\n" + f"FROM {source}\n" + f"WHERE j.value IS NOT NULL AND j.value != ''" + ) + return "\nUNION ALL\n".join(branches) + + +def _vc_tracked_columns(types: list[tuple[str, str]]) -> list[str]: + return [column for _, column in types] + ["path_cover_s", "path_cover_l"] + + +def _rebuild_vc_triggers(pg: bool, types: list[tuple[str, str]]) -> None: + tracked = _vc_tracked_columns(types) + if pg: + op.execute("DROP TRIGGER IF EXISTS virtual_collection_roms_aiu ON roms") + op.execute("DROP FUNCTION IF EXISTS romm_sync_virtual_collection_roms()") + unchanged = " AND ".join( + f"NEW.{c} IS NOT DISTINCT FROM OLD.{c}" for c in tracked + ) + op.execute(f""" +CREATE OR REPLACE FUNCTION romm_sync_virtual_collection_roms() RETURNS trigger + LANGUAGE plpgsql AS $$ +BEGIN + IF TG_OP = 'UPDATE' THEN + IF {unchanged} THEN + RETURN NULL; + END IF; + DELETE FROM {TABLE} WHERE rom_id = NEW.id; + END IF; + + INSERT INTO {TABLE} ({_VC_COLUMNS}) + {_postgres_vc_rows("NEW", False, types)} + ON CONFLICT DO NOTHING; + + RETURN NULL; +END $$ +""") # nosec B608 + op.execute( + "CREATE TRIGGER virtual_collection_roms_aiu AFTER INSERT OR UPDATE ON roms\n" + "FOR EACH ROW EXECUTE FUNCTION romm_sync_virtual_collection_roms()" + ) + else: + op.execute("DROP TRIGGER IF EXISTS virtual_collection_roms_ai") + op.execute("DROP TRIGGER IF EXISTS virtual_collection_roms_au") + insert = ( + f"INSERT IGNORE INTO {TABLE} ({_VC_COLUMNS})\n" # nosec B608 + f"{_mysql_vc_rows('NEW', False, types)}" + ) + unchanged = " AND ".join(f"NEW.{c} <=> OLD.{c}" for c in tracked) + op.execute( + f"CREATE TRIGGER virtual_collection_roms_ai AFTER INSERT ON roms\n" # nosec B608 + f"FOR EACH ROW\n{insert}" + ) + op.execute( + f"CREATE TRIGGER virtual_collection_roms_au AFTER UPDATE ON roms\n" # nosec B608 + f"FOR EACH ROW\nBEGIN\n" + f"IF NOT ({unchanged}) THEN\n" + f"DELETE FROM {TABLE} WHERE rom_id = NEW.id;\n" + f"{insert};\n" + f"END IF;\n" + f"END" + ) + + +# --------------------------------------------------------------------------- +# Migration +# --------------------------------------------------------------------------- + + +def upgrade() -> None: + pg = is_postgresql(op.get_bind()) + + _add_generated_columns(pg) + _rebuild_roms_metadata_view(pg, include_new=True) + + op.add_column("roms_facets", sa.Column("publishers", CustomJSON(), nullable=True)) + op.add_column("roms_facets", sa.Column("developers", CustomJSON(), nullable=True)) + _backfill_facets_new_columns(pg) + _rebuild_facets_triggers(pg, include_new=True) + + # Backfill only the new types so existing membership rows aren't duplicated. + rows = ( + _postgres_vc_rows("r", True, _VC_NEW_TYPES) + if pg + else _mysql_vc_rows("r", True, _VC_NEW_TYPES) + ) + conflict = "ON CONFLICT DO NOTHING" if pg else "" + insert = "INSERT INTO" if pg else "INSERT IGNORE INTO" + op.execute(f"{insert} {TABLE} ({_VC_COLUMNS})\n{rows}\n{conflict}") # nosec B608 + _rebuild_vc_triggers(pg, _VC_ALL_TYPES) + + +def downgrade() -> None: + pg = is_postgresql(op.get_bind()) + + op.execute( + f"DELETE FROM {TABLE} WHERE type IN ('publisher', 'developer')" # nosec B608 + ) + _rebuild_vc_triggers(pg, _VC_BASE_TYPES) + + _rebuild_facets_triggers(pg, include_new=False) + op.drop_column("roms_facets", "developers") + op.drop_column("roms_facets", "publishers") + + _rebuild_roms_metadata_view(pg, include_new=False) + _drop_generated_columns() diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index 75094a90bb..de082f6bf1 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -101,6 +101,8 @@ class UserNoteSchema(BaseModel): "genres": list[str] | None, "franchises": list[str] | None, "companies": list[str] | None, + "publishers": list[str] | None, + "developers": list[str] | None, "game_modes": list[str] | None, "age_ratings": list[str] | None, "first_release_date": int | None, @@ -219,6 +221,8 @@ class RomMetadataSchema(BaseModel): franchises: list[str] collections: list[str] companies: list[str] + publishers: list[str] + developers: list[str] game_modes: list[str] age_ratings: list[str] player_count: str @@ -241,6 +245,14 @@ def sort_collections(cls, v: list[str]) -> list[str]: def sort_companies(cls, v: list[str]) -> list[str]: return sorted(v) + @field_validator("publishers") + def sort_publishers(cls, v: list[str]) -> list[str]: + return sorted(v) + + @field_validator("developers") + def sort_developers(cls, v: list[str]) -> list[str]: + return sorted(v) + @field_validator("game_modes") def sort_game_modes(cls, v: list[str]) -> list[str]: return sorted(v) @@ -646,6 +658,8 @@ class RomFiltersDict(TypedDict): franchises: list[str] collections: list[str] companies: list[str] + publishers: list[str] + developers: list[str] game_modes: list[str] age_ratings: list[str] player_counts: list[str] diff --git a/backend/endpoints/roms/__init__.py b/backend/endpoints/roms/__init__.py index ba0472e08b..892944e916 100644 --- a/backend/endpoints/roms/__init__.py +++ b/backend/endpoints/roms/__init__.py @@ -437,6 +437,24 @@ def get_roms( ), ), ] = None, + publishers: Annotated[ + list[str] | None, + Query( + description=( + "Associated publisher. Multiple values are allowed by repeating" + " the parameter, and results that match any of the values will be returned." + ), + ), + ] = None, + developers: Annotated[ + list[str] | None, + Query( + description=( + "Associated developer. Multiple values are allowed by repeating" + " the parameter, and results that match any of the values will be returned." + ), + ), + ] = None, age_ratings: Annotated[ list[str] | None, Query( @@ -528,6 +546,18 @@ def get_roms( description="Logic operator for companies filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", ), ] = "any", + publishers_logic: Annotated[ + str, + Query( + description="Logic operator for publishers filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", + ), + ] = "any", + developers_logic: Annotated[ + str, + Query( + description="Logic operator for developers filter: 'any' (OR), 'all' (AND) or 'none' (NOT).", + ), + ] = "any", age_ratings_logic: Annotated[ str, Query( @@ -631,6 +661,8 @@ def get_roms( franchises=franchises, collections=collections, companies=companies, + publishers=publishers, + developers=developers, age_ratings=age_ratings, statuses=statuses, regions=regions, @@ -643,6 +675,8 @@ def get_roms( franchises_logic=franchises_logic, collections_logic=collections_logic, companies_logic=companies_logic, + publishers_logic=publishers_logic, + developers_logic=developers_logic, age_ratings_logic=age_ratings_logic, regions_logic=regions_logic, languages_logic=languages_logic, @@ -672,6 +706,8 @@ def get_roms( or franchises or collections or companies + or publishers + or developers or age_ratings or statuses or regions @@ -714,6 +750,8 @@ def get_roms( franchises=[], collections=[], companies=[], + publishers=[], + developers=[], game_modes=[], age_ratings=[], player_counts=[], diff --git a/backend/handler/database/collections_handler.py b/backend/handler/database/collections_handler.py index 3ef300c4d5..d2e17da642 100644 --- a/backend/handler/database/collections_handler.py +++ b/backend/handler/database/collections_handler.py @@ -453,6 +453,8 @@ def convert_legacy_filter(new_key: str, old_key: str) -> list[str] | None: franchises = convert_legacy_filter("franchises", "selected_franchise") collections = convert_legacy_filter("collections", "selected_collection") companies = convert_legacy_filter("companies", "selected_company") + publishers = convert_legacy_filter("publishers", "selected_publisher") + developers = convert_legacy_filter("developers", "selected_developer") age_ratings = convert_legacy_filter("age_ratings", "selected_age_rating") regions = convert_legacy_filter("regions", "selected_region") languages = convert_legacy_filter("languages", "selected_language") @@ -481,6 +483,8 @@ def convert_legacy_filter(new_key: str, old_key: str) -> list[str] | None: franchises=franchises, collections=collections, companies=companies, + publishers=publishers, + developers=developers, age_ratings=age_ratings, statuses=statuses, regions=regions, @@ -492,6 +496,8 @@ def convert_legacy_filter(new_key: str, old_key: str) -> list[str] | None: franchises_logic=criteria.get("franchises_logic", "any"), collections_logic=criteria.get("collections_logic", "any"), companies_logic=criteria.get("companies_logic", "any"), + publishers_logic=criteria.get("publishers_logic", "any"), + developers_logic=criteria.get("developers_logic", "any"), age_ratings_logic=criteria.get("age_ratings_logic", "any"), regions_logic=criteria.get("regions_logic", "any"), languages_logic=criteria.get("languages_logic", "any"), diff --git a/backend/handler/database/roms_handler.py b/backend/handler/database/roms_handler.py index 31f88462ab..55c219be54 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -135,6 +135,8 @@ RomFacets.franchises, RomFacets.collections, RomFacets.companies, + RomFacets.publishers, + RomFacets.developers, RomFacets.game_modes, RomFacets.age_ratings, RomFacets.player_count, @@ -729,6 +731,32 @@ def _filter_by_companies( condition = op(RomMetadata.companies, values, session=session) return query.filter(~condition) if match_none else query.filter(condition) + def _filter_by_publishers( + self, + query: Query, + *, + session: Session, + values: Sequence[str], + match_all: bool = False, + match_none: bool = False, + ) -> Query: + op = json_array_contains_all if match_all else json_array_contains_any + condition = op(RomMetadata.publishers, values, session=session) + return query.filter(~condition) if match_none else query.filter(condition) + + def _filter_by_developers( + self, + query: Query, + *, + session: Session, + values: Sequence[str], + match_all: bool = False, + match_none: bool = False, + ) -> Query: + op = json_array_contains_all if match_all else json_array_contains_any + condition = op(RomMetadata.developers, values, session=session) + return query.filter(~condition) if match_none else query.filter(condition) + def _filter_by_age_ratings( self, query: Query, @@ -888,6 +916,8 @@ def filter_roms( franchises: Sequence[str] | None = None, collections: Sequence[str] | None = None, companies: Sequence[str] | None = None, + publishers: Sequence[str] | None = None, + developers: Sequence[str] | None = None, age_ratings: Sequence[str] | None = None, statuses: Sequence[str] | None = None, regions: Sequence[str] | None = None, @@ -900,6 +930,8 @@ def filter_roms( franchises_logic: str = "any", collections_logic: str = "any", companies_logic: str = "any", + publishers_logic: str = "any", + developers_logic: str = "any", age_ratings_logic: str = "any", regions_logic: str = "any", languages_logic: str = "any", @@ -1131,7 +1163,16 @@ def filter_roms( # Optimize JOINs - only join tables when needed needs_metadata_join = any( - [genres, franchises, collections, companies, age_ratings, player_counts] + [ + genres, + franchises, + collections, + companies, + publishers, + developers, + age_ratings, + player_counts, + ] ) if needs_metadata_join: @@ -1143,6 +1184,8 @@ def filter_roms( (franchises, franchises_logic, self._filter_by_franchises), (collections, collections_logic, self._filter_by_collections), (companies, companies_logic, self._filter_by_companies), + (publishers, publishers_logic, self._filter_by_publishers), + (developers, developers_logic, self._filter_by_developers), (age_ratings, age_ratings_logic, self._filter_by_age_ratings), (regions, regions_logic, self._filter_by_regions), (languages, languages_logic, self._filter_by_languages), @@ -1289,6 +1332,8 @@ def get_roms_scalar( franchises=kwargs.get("franchises", None), collections=kwargs.get("collections", None), companies=kwargs.get("companies", None), + publishers=kwargs.get("publishers", None), + developers=kwargs.get("developers", None), age_ratings=kwargs.get("age_ratings", None), statuses=kwargs.get("statuses", None), regions=kwargs.get("regions", None), @@ -1301,6 +1346,8 @@ def get_roms_scalar( franchises_logic=kwargs.get("franchises_logic", "any"), collections_logic=kwargs.get("collections_logic", "any"), companies_logic=kwargs.get("companies_logic", "any"), + publishers_logic=kwargs.get("publishers_logic", "any"), + developers_logic=kwargs.get("developers_logic", "any"), age_ratings_logic=kwargs.get("age_ratings_logic", "any"), regions_logic=kwargs.get("regions_logic", "any"), languages_logic=kwargs.get("languages_logic", "any"), @@ -2285,6 +2332,8 @@ def _collect_filter_values( franchises = set() collections = set() companies = set() + publishers = set() + developers = set() game_modes = set() age_ratings = set() player_counts = set() @@ -2294,7 +2343,7 @@ def _collect_filter_values( platforms = set() for row in session.execute(statement): - g, f, cl, co, gm, ar, pc, rg, lg, tg, pid = row + g, f, cl, co, pub, dev, gm, ar, pc, rg, lg, tg, pid = row if g: genres.update(g) if f: @@ -2303,6 +2352,10 @@ def _collect_filter_values( collections.update(cl) if co: companies.update(co) + if pub: + publishers.update(pub) + if dev: + developers.update(dev) if gm: game_modes.update(gm) if ar: @@ -2322,6 +2375,8 @@ def _collect_filter_values( "franchises": sorted(franchises), "collections": sorted(collections), "companies": sorted(companies), + "publishers": sorted(publishers), + "developers": sorted(developers), "game_modes": sorted(game_modes), "age_ratings": sorted(age_ratings), "player_counts": sorted(player_counts), diff --git a/backend/handler/metadata/flashpoint_handler.py b/backend/handler/metadata/flashpoint_handler.py index c1fbbd034c..58e602b65f 100644 --- a/backend/handler/metadata/flashpoint_handler.py +++ b/backend/handler/metadata/flashpoint_handler.py @@ -46,6 +46,8 @@ class FlashpointGame(TypedDict): class FlashpointMetadata(TypedDict): franchises: list[str] companies: list[str] + publishers: list[str] + developers: list[str] source: str | None genres: list[str] first_release_date: str @@ -78,6 +80,8 @@ def extract_flashpoint_metadata(game: FlashpointGame) -> FlashpointMetadata: return FlashpointMetadata( franchises=pydash.compact([game["series"]]), companies=pydash.uniq(pydash.compact([game["developer"], game["publisher"]])), + publishers=pydash.compact([game["publisher"]]), + developers=pydash.compact([game["developer"]]), source=game["source"], genres=game["tags"], first_release_date=first_release_date, diff --git a/backend/handler/metadata/gamelist_handler.py b/backend/handler/metadata/gamelist_handler.py index d5b0893b72..973b746ee0 100644 --- a/backend/handler/metadata/gamelist_handler.py +++ b/backend/handler/metadata/gamelist_handler.py @@ -59,6 +59,8 @@ class GamelistMetadata(GamelistMetadataMedia): first_release_date: str | None sort_name: str | None companies: list[str] | None + publishers: list[str] | None + developers: list[str] | None franchises: list[str] | None genres: list[str] | None player_count: str | None @@ -235,6 +237,8 @@ def extract_metadata_from_gamelist_rom( ) ) ), + publishers=_split_comma_separated_values(publisher), + developers=_split_comma_separated_values(developer), franchises=_split_comma_separated_values(family), genres=_split_comma_separated_values(genre), player_count=players, diff --git a/backend/handler/metadata/hasheous_handler.py b/backend/handler/metadata/hasheous_handler.py index 3dd1133b13..5c5b06bac0 100644 --- a/backend/handler/metadata/hasheous_handler.py +++ b/backend/handler/metadata/hasheous_handler.py @@ -87,6 +87,16 @@ def extract_metadata_from_igdb_rom(rom: dict[str, Any]) -> IGDBMetadata: "companies": pydash.compact( pydash.map_(rom.get("involved_companies", {}), "company.name") ), + "publishers": pydash.compact( + c.get("company", {}).get("name") + for c in rom.get("involved_companies", []) + if c.get("publisher") + ), + "developers": pydash.compact( + c.get("company", {}).get("name") + for c in rom.get("involved_companies", []) + if c.get("developer") + ), "platforms": [ IGDBMetadataPlatform(igdb_id=p.get("id", ""), name=p.get("name", "")) for p in pydash.map_(rom.get("platforms", {})) diff --git a/backend/handler/metadata/igdb_handler.py b/backend/handler/metadata/igdb_handler.py index 9a11a54618..f121327895 100644 --- a/backend/handler/metadata/igdb_handler.py +++ b/backend/handler/metadata/igdb_handler.py @@ -126,6 +126,8 @@ class IGDBMetadata(TypedDict): alternative_names: list[str] collections: list[str] companies: list[str] + publishers: list[str] + developers: list[str] game_modes: list[str] age_ratings: list[IGDBAgeRating] platforms: list[IGDBMetadataPlatform] @@ -253,6 +255,16 @@ def extract_metadata_from_igdb_rom( "companies": [ c["company"]["name"] for c in involved_companies if c.get("company") ], + "publishers": [ + c["company"]["name"] + for c in involved_companies + if c.get("company") and c.get("publisher") + ], + "developers": [ + c["company"]["name"] + for c in involved_companies + if c.get("company") and c.get("developer") + ], "platforms": [ IGDBMetadataPlatform(igdb_id=p["id"], name=p.get("name", "")) for p in platforms @@ -1035,6 +1047,8 @@ async def get_oauth_token(self) -> str: "collections.name", "game_modes.name", "involved_companies.company.name", + "involved_companies.developer", + "involved_companies.publisher", "expansions.id", "expansions.slug", "expansions.name", diff --git a/backend/handler/metadata/launchbox_handler/media.py b/backend/handler/metadata/launchbox_handler/media.py index 624d979f28..3c8b79ecbc 100644 --- a/backend/handler/metadata/launchbox_handler/media.py +++ b/backend/handler/metadata/launchbox_handler/media.py @@ -594,6 +594,8 @@ def build_launchbox_metadata( "esrb": esrb, "genres": genres, "companies": companies, + "publishers": dedupe_words([publisher]), + "developers": dedupe_words([developer]), "images": images, } ) diff --git a/backend/handler/metadata/launchbox_handler/types.py b/backend/handler/metadata/launchbox_handler/types.py index 0a403dac2c..b64c64eca8 100644 --- a/backend/handler/metadata/launchbox_handler/types.py +++ b/backend/handler/metadata/launchbox_handler/types.py @@ -53,6 +53,8 @@ class LaunchboxMetadata(TypedDict): esrb: NotRequired[str] genres: NotRequired[list[str]] companies: NotRequired[list[str]] + publishers: NotRequired[list[str]] + developers: NotRequired[list[str]] images: list[LaunchboxImage] video_url: NotRequired[str] video_path: NotRequired[str] diff --git a/backend/handler/metadata/ra_handler.py b/backend/handler/metadata/ra_handler.py index 6273010aed..62a481d792 100644 --- a/backend/handler/metadata/ra_handler.py +++ b/backend/handler/metadata/ra_handler.py @@ -53,6 +53,8 @@ class RAMetadata(TypedDict): first_release_date: int | None genres: list[str] companies: list[str] + publishers: list[str] + developers: list[str] achievements: list[RAGameRomAchievement] @@ -103,6 +105,8 @@ def parse_release_timestamp(): companies=pydash.compact( [rom_details.get("Publisher", None), rom_details.get("Developer", None)] ), + publishers=pydash.compact([rom_details.get("Publisher", None)]), + developers=pydash.compact([rom_details.get("Developer", None)]), achievements=[ RAGameRomAchievement( ra_id=achievement.get("ID", None), diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 4f27a312f2..b5b6d7adb9 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -209,6 +209,8 @@ class SSMetadata(SSMetadataMedia): alternative_names: list[str] age_ratings: list[SSAgeRating] companies: list[str] + publishers: list[str] + developers: list[str] franchises: list[str] game_modes: list[str] genres: list[str] @@ -517,6 +519,8 @@ def _get_age_ratings(game: SSGame) -> list[SSAgeRating]: game.get("developpeur", {}).get("text"), ] ), + "publishers": pydash.compact([game.get("editeur", {}).get("text")]), + "developers": pydash.compact([game.get("developpeur", {}).get("text")]), "genres": _get_genres(game), "first_release_date": _get_lowest_date(game.get("dates", [])), "franchises": _get_franchises(game), diff --git a/backend/models/rom.py b/backend/models/rom.py index 957d01ad36..a6b7d886aa 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -225,6 +225,8 @@ class RomMetadata(BaseModel): franchises: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) collections: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) companies: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) + publishers: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) + developers: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) game_modes: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) age_ratings: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) player_count: Mapped[str | None] = mapped_column(String(length=100), default="1") @@ -257,6 +259,8 @@ class RomFacets(BaseModel): franchises: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) collections: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) companies: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) + publishers: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) + developers: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) game_modes: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) age_ratings: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[]) player_count: Mapped[str | None] = mapped_column(String(length=100), default="1") diff --git a/backend/tests/handler/database/test_generated_metadata.py b/backend/tests/handler/database/test_generated_metadata.py index b53364023e..ee145c6893 100644 --- a/backend/tests/handler/database/test_generated_metadata.py +++ b/backend/tests/handler/database/test_generated_metadata.py @@ -44,6 +44,36 @@ def test_igdb_derivations(self, rom: Rom): # Quoted-string epoch seconds -> ms. The old view truncated this to 0. assert meta.first_release_date == 1569369600000 + def test_publishers_developers_derivation(self, rom: Rom): + db_rom_handler.update_rom( + rom.id, + { + "ss_metadata": { + "companies": ["Atari", "Artech Studios"], + "publishers": ["Atari"], + "developers": ["Artech Studios"], + }, + }, + ) + + meta = _reload(rom).metadatum + # Raw view order preserved here (the response schema sorts on serialize). + assert meta.companies == ["Atari", "Artech Studios"] + assert meta.publishers == ["Atari"] + assert meta.developers == ["Artech Studios"] + + def test_publishers_developers_default_empty(self, rom: Rom): + # companies present but no split fields -> new fields stay empty. + db_rom_handler.update_rom( + rom.id, + {"ss_metadata": {"companies": ["Nintendo"]}}, + ) + + meta = _reload(rom).metadatum + assert meta.companies == ["Nintendo"] + assert meta.publishers == [] + assert meta.developers == [] + def test_rating_average_across_providers(self, rom: Rom): db_rom_handler.update_rom( rom.id, diff --git a/backend/tests/handler/database/test_rom_facets.py b/backend/tests/handler/database/test_rom_facets.py index 126acc4301..f339eef6b9 100644 --- a/backend/tests/handler/database/test_rom_facets.py +++ b/backend/tests/handler/database/test_rom_facets.py @@ -40,6 +40,27 @@ def test_update_mirrors_derived_and_raw_values(self, rom: Rom): assert facets.regions == ["USA"] assert facets.tags == ["Proto"] + def test_publishers_developers_mirror(self, rom: Rom): + db_rom_handler.update_rom( + rom.id, + { + "ss_metadata": { + "companies": ["Atari", "Artech Studios"], + "publishers": ["Atari"], + "developers": ["Artech Studios"], + } + }, + ) + + facets = _facets(rom.id) + assert facets is not None + assert facets.publishers == ["Atari"] + assert facets.developers == ["Artech Studios"] + + filters = db_rom_handler.get_rom_filters() + assert "Atari" in filters["publishers"] + assert "Artech Studios" in filters["developers"] + def test_delete_cascades(self, rom: Rom): rom_id = rom.id db_rom_handler.delete_rom(rom_id) diff --git a/backend/tests/utils/test_gamelist_exporter.py b/backend/tests/utils/test_gamelist_exporter.py index 7bbbc91693..14be80b73f 100644 --- a/backend/tests/utils/test_gamelist_exporter.py +++ b/backend/tests/utils/test_gamelist_exporter.py @@ -128,6 +128,31 @@ def test_export_gamelist_xml_basic(platform_with_roms): assert players.text == "2" +def test_export_gamelist_prefers_explicit_publisher_developer(platform_with_roms): + platform, roms = platform_with_roms + # The companies order would map developer=Nintendo / publisher=Nintendo EAD; + # the explicit split fields (deliberately reversed) must take precedence. + db_rom_handler.update_rom( + roms[0].id, + { + "igdb_metadata": { + "companies": ["Nintendo", "Nintendo EAD"], + "publishers": ["Nintendo"], + "developers": ["Nintendo EAD"], + } + }, + ) + + xml_str = GamelistExporter(local_export=True).export_platform_to_xml( + platform.id, request=None + ) + game = fromstring(xml_str).findall("game")[0] + developer = game.find("developer") + publisher = game.find("publisher") + assert developer is not None and developer.text == "Nintendo EAD" + assert publisher is not None and publisher.text == "Nintendo" + + def test_export_gamelist_xml_rating(platform_with_roms): platform, _ = platform_with_roms exporter = GamelistExporter(local_export=True) diff --git a/backend/tests/utils/test_pegasus_exporter.py b/backend/tests/utils/test_pegasus_exporter.py index bf82e5b7ad..e201d09d5d 100644 --- a/backend/tests/utils/test_pegasus_exporter.py +++ b/backend/tests/utils/test_pegasus_exporter.py @@ -159,6 +159,45 @@ def test_full_metadata(self, admin_user: User): assert "x-romm-id" in game assert "sort-by" not in game + def test_prefers_explicit_publisher_developer(self, admin_user: User): + platform = Platform(name="NES", slug="nes", fs_slug="nes") + platform = db_platform_handler.add_platform(platform) + + rom = Rom( + platform_id=platform.id, + name="Test Game", + slug="test-game", + fs_name="test.nes", + fs_name_no_tags="test", + fs_name_no_ext="test", + fs_extension="nes", + fs_path="nes/roms", + ) + rom = db_rom_handler.add_rom(rom) + db_rom_handler.add_rom_user(rom_id=rom.id, user_id=admin_user.id) + + # companies order would give developer=Atari / publisher=Artech; the + # explicit split fields (reversed here) must take precedence. + db_rom_handler.update_rom( + rom.id, + { + "igdb_metadata": { + "companies": ["Atari", "Artech Studios"], + "publishers": ["Atari"], + "developers": ["Artech Studios"], + } + }, + ) + + parsed = _parse_pegasus( + PegasusExporter(local_export=True).export_platform_to_pegasus( + platform.id, request=None + ) + ) + game = parsed["games"][0] + assert game["developer"] == "Artech Studios" + assert game["publisher"] == "Atari" + def test_minimal_rom(self, admin_user: User): platform = Platform(name="Game Boy", slug="gb", fs_slug="gb") platform = db_platform_handler.add_platform(platform) @@ -399,6 +438,7 @@ def test_copy_asset_skips_existing(self, tmp_path): def test_game_entry_with_assets(self): metadatum = MagicMock() metadatum.companies = metadatum.genres = metadatum.player_count = None + metadatum.publishers = metadatum.developers = None metadatum.first_release_date = metadatum.average_rating = None rom = _mock_rom( diff --git a/backend/utils/gamelist_exporter.py b/backend/utils/gamelist_exporter.py index 1607c02c93..e5fb4da5b0 100644 --- a/backend/utils/gamelist_exporter.py +++ b/backend/utils/gamelist_exporter.py @@ -221,12 +221,20 @@ def _create_game_element( if "manual" in asset_refs: SubElement(game, "manual").text = asset_refs["manual"] - # Additional metadata - if rom.metadatum.companies and len(rom.metadatum.companies) > 0: - SubElement(game, "developer").text = rom.metadatum.companies[0] - - if rom.metadatum.companies and len(rom.metadatum.companies) > 1: - SubElement(game, "publisher").text = rom.metadatum.companies[1] + # Additional metadata. Prefer the explicit fields; fall back to the old + # companies[0]=developer / companies[1]=publisher heuristic for ROMs + # scanned before the split populated publishers/developers. + companies = rom.metadatum.companies or [] + developer = (rom.metadatum.developers or [None])[0] or ( + companies[0] if len(companies) > 0 else None + ) + publisher = (rom.metadatum.publishers or [None])[0] or ( + companies[1] if len(companies) > 1 else None + ) + if developer: + SubElement(game, "developer").text = developer + if publisher: + SubElement(game, "publisher").text = publisher if rom.metadatum.genres and len(rom.metadatum.genres) > 0: SubElement(game, "genre").text = rom.metadatum.genres[0] diff --git a/backend/utils/pegasus_exporter.py b/backend/utils/pegasus_exporter.py index 17d32afc82..c8f779cec2 100644 --- a/backend/utils/pegasus_exporter.py +++ b/backend/utils/pegasus_exporter.py @@ -238,12 +238,21 @@ def _create_game_entry( if rom.name and rom.fs_name_no_tags and rom.name != rom.fs_name_no_tags: lines.append(f"sort-by: {rom.fs_name_no_tags}") - # Developers and publishers - if rom.metadatum and rom.metadatum.companies: - if len(rom.metadatum.companies) > 0: - lines.append(f"developer: {rom.metadatum.companies[0]}") - if len(rom.metadatum.companies) > 1: - lines.append(f"publisher: {rom.metadatum.companies[1]}") + # Developers and publishers. Prefer the explicit fields; fall back to + # the old companies[0]=developer / companies[1]=publisher heuristic for + # ROMs scanned before the split populated publishers/developers. + if rom.metadatum: + companies = rom.metadatum.companies or [] + developer = (rom.metadatum.developers or [None])[0] or ( + companies[0] if len(companies) > 0 else None + ) + publisher = (rom.metadatum.publishers or [None])[0] or ( + companies[1] if len(companies) > 1 else None + ) + if developer: + lines.append(f"developer: {developer}") + if publisher: + lines.append(f"publisher: {publisher}") # Genres if rom.metadatum and rom.metadatum.genres: diff --git a/frontend/src/__generated__/models/ManualMetadata.ts b/frontend/src/__generated__/models/ManualMetadata.ts index 49e3903ddc..d0f72c1a49 100644 --- a/frontend/src/__generated__/models/ManualMetadata.ts +++ b/frontend/src/__generated__/models/ManualMetadata.ts @@ -6,6 +6,8 @@ export type ManualMetadata = { genres?: (Array | null); franchises?: (Array | null); companies?: (Array | null); + publishers?: (Array | null); + developers?: (Array | null); game_modes?: (Array | null); age_ratings?: (Array | null); first_release_date?: (number | null); diff --git a/frontend/src/__generated__/models/RomFiltersDict.ts b/frontend/src/__generated__/models/RomFiltersDict.ts index bc56d30b13..4f989dc54a 100644 --- a/frontend/src/__generated__/models/RomFiltersDict.ts +++ b/frontend/src/__generated__/models/RomFiltersDict.ts @@ -7,6 +7,8 @@ export type RomFiltersDict = { franchises: Array; collections: Array; companies: Array; + publishers: Array; + developers: Array; game_modes: Array; age_ratings: Array; player_counts: Array; diff --git a/frontend/src/__generated__/models/RomFlashpointMetadata.ts b/frontend/src/__generated__/models/RomFlashpointMetadata.ts index 0f5ed5213d..35d4734374 100644 --- a/frontend/src/__generated__/models/RomFlashpointMetadata.ts +++ b/frontend/src/__generated__/models/RomFlashpointMetadata.ts @@ -5,6 +5,8 @@ export type RomFlashpointMetadata = { franchises?: Array; companies?: Array; + publishers?: Array; + developers?: Array; source?: (string | null); genres?: Array; first_release_date?: string; diff --git a/frontend/src/__generated__/models/RomGamelistMetadata.ts b/frontend/src/__generated__/models/RomGamelistMetadata.ts index d371b1a770..e34f4ab26f 100644 --- a/frontend/src/__generated__/models/RomGamelistMetadata.ts +++ b/frontend/src/__generated__/models/RomGamelistMetadata.ts @@ -20,6 +20,8 @@ export type RomGamelistMetadata = { first_release_date?: (string | null); sort_name?: (string | null); companies?: (Array | null); + publishers?: (Array | null); + developers?: (Array | null); franchises?: (Array | null); genres?: (Array | null); player_count?: (string | null); diff --git a/frontend/src/__generated__/models/RomIGDBMetadata.ts b/frontend/src/__generated__/models/RomIGDBMetadata.ts index d924f2732a..2418133e3f 100644 --- a/frontend/src/__generated__/models/RomIGDBMetadata.ts +++ b/frontend/src/__generated__/models/RomIGDBMetadata.ts @@ -16,6 +16,8 @@ export type RomIGDBMetadata = { alternative_names?: Array; collections?: Array; companies?: Array; + publishers?: Array; + developers?: Array; game_modes?: Array; age_ratings?: Array; platforms?: Array; diff --git a/frontend/src/__generated__/models/RomLaunchboxMetadata.ts b/frontend/src/__generated__/models/RomLaunchboxMetadata.ts index b650b01846..c03dc38c1e 100644 --- a/frontend/src/__generated__/models/RomLaunchboxMetadata.ts +++ b/frontend/src/__generated__/models/RomLaunchboxMetadata.ts @@ -15,6 +15,8 @@ export type RomLaunchboxMetadata = { esrb?: string; genres?: Array; companies?: Array; + publishers?: Array; + developers?: Array; images?: Array; video_url?: string; video_path?: string; diff --git a/frontend/src/__generated__/models/RomMetadataSchema.ts b/frontend/src/__generated__/models/RomMetadataSchema.ts index 5a8950a8b2..0496991cce 100644 --- a/frontend/src/__generated__/models/RomMetadataSchema.ts +++ b/frontend/src/__generated__/models/RomMetadataSchema.ts @@ -8,6 +8,8 @@ export type RomMetadataSchema = { franchises: Array; collections: Array; companies: Array; + publishers: Array; + developers: Array; game_modes: Array; age_ratings: Array; player_count: string; diff --git a/frontend/src/__generated__/models/RomRAMetadata.ts b/frontend/src/__generated__/models/RomRAMetadata.ts index a294ee449d..3a7f38148e 100644 --- a/frontend/src/__generated__/models/RomRAMetadata.ts +++ b/frontend/src/__generated__/models/RomRAMetadata.ts @@ -7,6 +7,8 @@ export type RomRAMetadata = { first_release_date?: (number | null); genres?: Array; companies?: Array; + publishers?: Array; + developers?: Array; achievements?: Array; }; diff --git a/frontend/src/__generated__/models/RomSSMetadata.ts b/frontend/src/__generated__/models/RomSSMetadata.ts index d0377f04b7..0a4ba0805d 100644 --- a/frontend/src/__generated__/models/RomSSMetadata.ts +++ b/frontend/src/__generated__/models/RomSSMetadata.ts @@ -40,6 +40,8 @@ export type RomSSMetadata = { alternative_names?: Array; age_ratings?: Array; companies?: Array; + publishers?: Array; + developers?: Array; franchises?: Array; game_modes?: Array; genres?: Array; diff --git a/frontend/src/locales/bg_BG/platform.json b/frontend/src/locales/bg_BG/platform.json index fb49ca52c5..bad153d38c 100644 --- a/frontend/src/locales/bg_BG/platform.json +++ b/frontend/src/locales/bg_BG/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Изтрий платформата", "delete-platform-hint": "Премахва платформата и нейните записи в ROM базата данни. Файловете на диска НЕ се изтриват.", "description": "Описание", + "developer": "Developer", "download-platform": "Изтегли платформа", "edit-platform": "Редактирай платформата", "export": "Експортирай", @@ -48,6 +49,7 @@ "old-squared-cases": "Стари квадратни кутии", "on-disk": "На диск", "player-count": "Брой играчи", + "publisher": "Publisher", "random-rom": "Случаен ROM", "random-rom-empty": "Няма ROM в тази платформа", "random-rom-error": "Неуспешно избиране на случаен ROM", diff --git a/frontend/src/locales/bg_BG/rom.json b/frontend/src/locales/bg_BG/rom.json index b8d8775f0a..3d0d612b64 100644 --- a/frontend/src/locales/bg_BG/rom.json +++ b/frontend/src/locales/bg_BG/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM изтрит от базата данни | {count} ROM-а изтрити от базата данни", "deleted-from-filesystem": "{count} ROM изтрит от файловата система | {count} ROM-а изтрити от файловата система", "details": "Детайли", + "developers": "Developers", "difficulty": "Трудност", "disc": "Disc", "download": "Свали", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Публично", "public-notes": "Публични бележки", + "publishers": "Publishers", "qr-scan-to-download": "Сканирай, за да свалиш", "rating": "Оценка", "redownload": "Свали отново", diff --git a/frontend/src/locales/bg_BG/settings.json b/frontend/src/locales/bg_BG/settings.json index 766d1ec109..d758fea7d9 100644 --- a/frontend/src/locales/bg_BG/settings.json +++ b/frontend/src/locales/bg_BG/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Папка с варианти", "vc-all": "Всички (бавно)", "vc-collection": "Колекция", - "vc-company": "Разработчик", + "vc-company": "Компания", + "vc-developer": "Developer", "vc-franchise": "Франчайз", "vc-genre": "Жанр", "vc-mode": "Режим на игра", + "vc-publisher": "Publisher", "virtual-collection-type": "Тип автоматично генерирана колекция (метод за групиране на ROM-ове)", "watcher": "Наблюдател", "widget-library-stats": "Статистика на библиотеката", diff --git a/frontend/src/locales/cs_CZ/platform.json b/frontend/src/locales/cs_CZ/platform.json index c4e5626933..2974bcaf82 100644 --- a/frontend/src/locales/cs_CZ/platform.json +++ b/frontend/src/locales/cs_CZ/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Smazat platformu", "delete-platform-hint": "Odstraní platformu a její záznamy ROM z databáze. Soubory na disku NEBUDOU smazány.", "description": "Popis", + "developer": "Developer", "download-platform": "Stáhnout platformu", "edit-platform": "Upravit platformu", "export": "Export", @@ -48,6 +49,7 @@ "old-squared-cases": "Staré čtvercové obaly", "on-disk": "Na disku", "player-count": "Počet hráčů", + "publisher": "Publisher", "random-rom": "Náhodné ROM", "random-rom-empty": "Na této platformě nejsou žádné ROM", "random-rom-error": "Nepodařilo se vybrat náhodné ROM", diff --git a/frontend/src/locales/cs_CZ/rom.json b/frontend/src/locales/cs_CZ/rom.json index 9c96f8615f..a0f7944e46 100644 --- a/frontend/src/locales/cs_CZ/rom.json +++ b/frontend/src/locales/cs_CZ/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM odstraněno z databáze | {count} ROM odstraněna z databáze | {count} ROM odstraněny z databáze | {count} ROM odstraněno z databáze", "deleted-from-filesystem": "{count} ROM odstraněno ze souborového systému | {count} ROM odstraněna ze souborového systému | {count} ROM odstraněny ze souborového systému | {count} ROM odstraněno ze souborového systému", "details": "Detaily", + "developers": "Developers", "difficulty": "Obtížnost", "disc": "Disc", "download": "Stáhnout", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Veřejné", "public-notes": "Veřejné poznámky", + "publishers": "Publishers", "qr-scan-to-download": "Naskenujte pro stažení", "rating": "Hodnocení", "redownload": "Znovu stáhnout", diff --git a/frontend/src/locales/cs_CZ/settings.json b/frontend/src/locales/cs_CZ/settings.json index 01cc8abf15..63ef9b3da7 100644 --- a/frontend/src/locales/cs_CZ/settings.json +++ b/frontend/src/locales/cs_CZ/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Varianta složky", "vc-all": "Vše (pomalé)", "vc-collection": "Kolekce", - "vc-company": "Vývojář", + "vc-company": "Společnost", + "vc-developer": "Developer", "vc-franchise": "Franšíza", "vc-genre": "Žánr", "vc-mode": "Herní režim", + "vc-publisher": "Publisher", "virtual-collection-type": "Typ virtuální kolekce (způsob seskupování ROM)", "watcher": "Sledovač", "widget-library-stats": "Statistiky knihovny", diff --git a/frontend/src/locales/de_DE/platform.json b/frontend/src/locales/de_DE/platform.json index e1ae424bb9..e5d592c490 100644 --- a/frontend/src/locales/de_DE/platform.json +++ b/frontend/src/locales/de_DE/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Plattform löschen", "delete-platform-hint": "Entfernt die Plattform und ihre ROM-Datenbankeinträge. Dateien auf dem Datenträger werden NICHT gelöscht.", "description": "Beschreibung", + "developer": "Developer", "download-platform": "Plattform herunterladen", "edit-platform": "Plattform bearbeiten", "export": "Exportieren", @@ -48,6 +49,7 @@ "old-squared-cases": "Alte quadratische Hüllen", "on-disk": "Auf Datenträger", "player-count": "Spieleranzahl", + "publisher": "Publisher", "random-rom": "Zufälliges ROM", "random-rom-empty": "Keine ROMs auf dieser Plattform", "random-rom-error": "Zufälliges ROM konnte nicht ausgewählt werden", diff --git a/frontend/src/locales/de_DE/rom.json b/frontend/src/locales/de_DE/rom.json index 48c6d6efa8..3a1e61bbdd 100644 --- a/frontend/src/locales/de_DE/rom.json +++ b/frontend/src/locales/de_DE/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM aus der Datenbank gelöscht | {count} ROMs aus der Datenbank gelöscht", "deleted-from-filesystem": "{count} ROM aus dem Dateisystem gelöscht | {count} ROMs aus dem Dateisystem gelöscht", "details": "Details", + "developers": "Developers", "difficulty": "Schwierigkeitsgrad", "disc": "Disc", "download": "Herunterladen", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB-ID", "public": "Öffentlich", "public-notes": "Öffentliche Notizen", + "publishers": "Publishers", "qr-scan-to-download": "Scannen zum Herunterladen", "rating": "Bewertung", "redownload": "Erneut herunterladen", diff --git a/frontend/src/locales/de_DE/settings.json b/frontend/src/locales/de_DE/settings.json index ad567ec7cb..ff579e52e3 100644 --- a/frontend/src/locales/de_DE/settings.json +++ b/frontend/src/locales/de_DE/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Variantenordner", "vc-all": "Alle (langsam)", "vc-collection": "Sammlung", - "vc-company": "Entwickler", + "vc-company": "Unternehmen", + "vc-developer": "Developer", "vc-franchise": "Franchise", "vc-genre": "Genre", "vc-mode": "Spielmodus", + "vc-publisher": "Publisher", "virtual-collection-type": "Typ der automatisch generierten Sammlung (ROM-Gruppierungsmethode)", "watcher": "Überwacher", "widget-library-stats": "Bibliotheksstatistik", diff --git a/frontend/src/locales/en_GB/platform.json b/frontend/src/locales/en_GB/platform.json index 4c445d6f5f..9aa8c4e4bf 100644 --- a/frontend/src/locales/en_GB/platform.json +++ b/frontend/src/locales/en_GB/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Delete platform", "delete-platform-hint": "Removes the platform and its ROM database entries. Files on disk are NOT deleted.", "description": "Description", + "developer": "Developer", "download-platform": "Download platform", "edit-platform": "Edit platform", "export": "Export", @@ -48,6 +49,7 @@ "old-squared-cases": "Old squared cases", "on-disk": "On disk", "player-count": "Player count", + "publisher": "Publisher", "random-rom": "Random ROM", "random-rom-empty": "No ROMs in this platform", "random-rom-error": "Couldn't pick a random ROM", diff --git a/frontend/src/locales/en_GB/rom.json b/frontend/src/locales/en_GB/rom.json index 947156ea95..7ab34d44c4 100644 --- a/frontend/src/locales/en_GB/rom.json +++ b/frontend/src/locales/en_GB/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM deleted from the database | {count} ROMs deleted from the database", "deleted-from-filesystem": "{count} ROM deleted from the filesystem | {count} ROMs deleted from the filesystem", "details": "Details", + "developers": "Developers", "difficulty": "Difficulty", "disc": "Disc", "download": "Download", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Public", "public-notes": "Public notes", + "publishers": "Publishers", "qr-scan-to-download": "Scan to download", "rating": "Rating", "redownload": "Re-download", diff --git a/frontend/src/locales/en_GB/settings.json b/frontend/src/locales/en_GB/settings.json index 16be2d88b3..f354d41c0f 100644 --- a/frontend/src/locales/en_GB/settings.json +++ b/frontend/src/locales/en_GB/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Variant folder", "vc-all": "All (slow)", "vc-collection": "Collection", - "vc-company": "Developer", + "vc-company": "Company", + "vc-developer": "Developer", "vc-franchise": "Franchise", "vc-genre": "Genre", "vc-mode": "Play Mode", + "vc-publisher": "Publisher", "virtual-collection-type": "Virtual collection type (ROM grouping method)", "watcher": "Watcher", "widget-library-stats": "Library stats", diff --git a/frontend/src/locales/en_US/platform.json b/frontend/src/locales/en_US/platform.json index 4c445d6f5f..9aa8c4e4bf 100644 --- a/frontend/src/locales/en_US/platform.json +++ b/frontend/src/locales/en_US/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Delete platform", "delete-platform-hint": "Removes the platform and its ROM database entries. Files on disk are NOT deleted.", "description": "Description", + "developer": "Developer", "download-platform": "Download platform", "edit-platform": "Edit platform", "export": "Export", @@ -48,6 +49,7 @@ "old-squared-cases": "Old squared cases", "on-disk": "On disk", "player-count": "Player count", + "publisher": "Publisher", "random-rom": "Random ROM", "random-rom-empty": "No ROMs in this platform", "random-rom-error": "Couldn't pick a random ROM", diff --git a/frontend/src/locales/en_US/rom.json b/frontend/src/locales/en_US/rom.json index 1f86d9e45d..5c12d4e265 100644 --- a/frontend/src/locales/en_US/rom.json +++ b/frontend/src/locales/en_US/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM deleted from the database | {count} ROMs deleted from the database", "deleted-from-filesystem": "{count} ROM deleted from the filesystem | {count} ROMs deleted from the filesystem", "details": "Details", + "developers": "Developers", "difficulty": "Difficulty", "disc": "Disc", "download": "Download", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Public", "public-notes": "Public notes", + "publishers": "Publishers", "qr-scan-to-download": "Scan to download", "rating": "Rating", "redownload": "Re-download", diff --git a/frontend/src/locales/en_US/settings.json b/frontend/src/locales/en_US/settings.json index c2ed8e05bf..c33f1a91da 100644 --- a/frontend/src/locales/en_US/settings.json +++ b/frontend/src/locales/en_US/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Variant folder", "vc-all": "All (slow)", "vc-collection": "Collection", - "vc-company": "Developer", + "vc-company": "Company", + "vc-developer": "Developer", "vc-franchise": "Franchise", "vc-genre": "Genre", "vc-mode": "Play Mode", + "vc-publisher": "Publisher", "virtual-collection-type": "Virtual collection type (ROM grouping method)", "watcher": "Watcher", "widget-library-stats": "Library stats", diff --git a/frontend/src/locales/es_ES/platform.json b/frontend/src/locales/es_ES/platform.json index 748ffbdd3c..125f774cc3 100644 --- a/frontend/src/locales/es_ES/platform.json +++ b/frontend/src/locales/es_ES/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Eliminar platforma", "delete-platform-hint": "Elimina la plataforma y sus entradas de ROM. Los archivos en disco NO se eliminan.", "description": "Descripción", + "developer": "Developer", "download-platform": "Descargar plataforma", "edit-platform": "Editar plataforma", "export": "Exportar", @@ -48,6 +49,7 @@ "old-squared-cases": "Cajas cuadradas antiguas", "on-disk": "En disco", "player-count": "Número de jugadores", + "publisher": "Publisher", "random-rom": "ROM aleatorio", "random-rom-empty": "No hay ROMs en esta plataforma", "random-rom-error": "No se pudo elegir un ROM aleatorio", diff --git a/frontend/src/locales/es_ES/rom.json b/frontend/src/locales/es_ES/rom.json index 720a8f04af..dc5ff0710c 100644 --- a/frontend/src/locales/es_ES/rom.json +++ b/frontend/src/locales/es_ES/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM eliminada de la base de datos | {count} ROMs eliminadas de la base de datos", "deleted-from-filesystem": "{count} ROM eliminada del sistema de archivos | {count} ROMs eliminadas del sistema de archivos", "details": "Detalles", + "developers": "Developers", "difficulty": "Dificultad", "disc": "Disc", "download": "Descargar", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "ID de SteamGridDB", "public": "Pública", "public-notes": "Notas públicas", + "publishers": "Publishers", "qr-scan-to-download": "Escanear para descargar", "rating": "Valoración", "redownload": "Volver a descargar", diff --git a/frontend/src/locales/es_ES/settings.json b/frontend/src/locales/es_ES/settings.json index 7746d9b5f2..c5ea33bfe5 100644 --- a/frontend/src/locales/es_ES/settings.json +++ b/frontend/src/locales/es_ES/settings.json @@ -475,9 +475,11 @@ "vc-all": "Todo", "vc-collection": "Colección", "vc-company": "Compañía", + "vc-developer": "Developer", "vc-franchise": "Franquicia", "vc-genre": "Género", "vc-mode": "Modo de juego", + "vc-publisher": "Publisher", "virtual-collection-type": "Tipo de colección virtual (método de agrupación de ROM)", "watcher": "Vigilante", "widget-library-stats": "Estadísticas de la biblioteca", diff --git a/frontend/src/locales/fr_FR/platform.json b/frontend/src/locales/fr_FR/platform.json index 4a52f23c2c..88da2289a1 100644 --- a/frontend/src/locales/fr_FR/platform.json +++ b/frontend/src/locales/fr_FR/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Supprimer la plateforme", "delete-platform-hint": "Supprime la plateforme et ses entrées de la base de données des ROMs. Les fichiers sur le disque ne sont PAS supprimés.", "description": "Description", + "developer": "Developer", "download-platform": "Télécharger la plateforme", "edit-platform": "Modifier la plateforme", "export": "Exporter", @@ -48,6 +49,7 @@ "old-squared-cases": "Anciens boîtiers carrés", "on-disk": "Sur le disque", "player-count": "Nombre de joueurs", + "publisher": "Publisher", "random-rom": "ROM aléatoire", "random-rom-empty": "Aucune ROM sur cette plateforme", "random-rom-error": "Impossible de choisir une ROM aléatoire", diff --git a/frontend/src/locales/fr_FR/rom.json b/frontend/src/locales/fr_FR/rom.json index bb61b287de..5a95044b0e 100644 --- a/frontend/src/locales/fr_FR/rom.json +++ b/frontend/src/locales/fr_FR/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM supprimée de la base de données | {count} ROMs supprimées de la base de données", "deleted-from-filesystem": "{count} ROM supprimée du système de fichiers | {count} ROMs supprimées du système de fichiers", "details": "Détails", + "developers": "Developers", "difficulty": "Difficulté", "disc": "Disc", "download": "Télécharger", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "ID SteamGridDB", "public": "Publique", "public-notes": "Notes publiques", + "publishers": "Publishers", "qr-scan-to-download": "Scanner pour télécharger", "rating": "Note", "redownload": "Retélécharger", diff --git a/frontend/src/locales/fr_FR/settings.json b/frontend/src/locales/fr_FR/settings.json index 2c674de6bd..6b12dae976 100644 --- a/frontend/src/locales/fr_FR/settings.json +++ b/frontend/src/locales/fr_FR/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Dossier variante", "vc-all": "Tout (lent)", "vc-collection": "Collection", - "vc-company": "Développeur", + "vc-company": "Entreprise", + "vc-developer": "Developer", "vc-franchise": "Franchise", "vc-genre": "Genre", "vc-mode": "Mode de jeu", + "vc-publisher": "Publisher", "virtual-collection-type": "Type de collection virtuelle (méthode de regroupement des roms)", "watcher": "Surveilleur", "widget-library-stats": "Statistiques de la bibliothèque", diff --git a/frontend/src/locales/hu_HU/platform.json b/frontend/src/locales/hu_HU/platform.json index c1d2e99607..c6d8139eb1 100644 --- a/frontend/src/locales/hu_HU/platform.json +++ b/frontend/src/locales/hu_HU/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Platform törlése", "delete-platform-hint": "Eltávolítja a platformot és a ROM-adatbázis bejegyzéseit. A lemezen lévő fájlok NEM törlődnek.", "description": "Leírás", + "developer": "Developer", "download-platform": "Platform letöltése", "edit-platform": "Platform szerkesztése", "export": "Exportálás", @@ -48,6 +49,7 @@ "old-squared-cases": "Régi négyzetes tokok", "on-disk": "Lemezen", "player-count": "Játékosok száma", + "publisher": "Publisher", "random-rom": "Véletlenszerű ROM", "random-rom-empty": "Nincs ROM ezen a platformon", "random-rom-error": "Nem sikerült véletlenszerű ROM-ot választani", diff --git a/frontend/src/locales/hu_HU/rom.json b/frontend/src/locales/hu_HU/rom.json index 90e2ed4cea..f0eff21e0b 100644 --- a/frontend/src/locales/hu_HU/rom.json +++ b/frontend/src/locales/hu_HU/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM törlése az adatbázisból| {count} ROM-ok törölve az adatbázisból", "deleted-from-filesystem": "{count} ROM törlése az fájlrendszerből | {count} ROM-ok törölve a fájlrendszerből", "details": "Részletek", + "developers": "Developers", "difficulty": "Nehézség", "disc": "Disc", "download": "Letöltés", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB azonosító", "public": "Publikus", "public-notes": "Publikus jegyzetek", + "publishers": "Publishers", "qr-scan-to-download": "Olvasd be a letöltéshez", "rating": "Értékelés", "redownload": "Újraletöltés", diff --git a/frontend/src/locales/hu_HU/settings.json b/frontend/src/locales/hu_HU/settings.json index dac7146fc4..07e0e0f999 100644 --- a/frontend/src/locales/hu_HU/settings.json +++ b/frontend/src/locales/hu_HU/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Változat mappa", "vc-all": "Összes (lassú)", "vc-collection": " yűjtemény", - "vc-company": "Fejlesztő", + "vc-company": "Cég", + "vc-developer": "Developer", "vc-franchise": "Franchise", "vc-genre": "Műfaj", "vc-mode": "Játékmód", + "vc-publisher": "Publisher", "virtual-collection-type": "Virtuális gyűjtemény típusa (ROM csoportosítási mód)", "watcher": "Figyelő", "widget-library-stats": "Könyvtárstatisztika", diff --git a/frontend/src/locales/it_IT/platform.json b/frontend/src/locales/it_IT/platform.json index 23f9811770..8faa047933 100644 --- a/frontend/src/locales/it_IT/platform.json +++ b/frontend/src/locales/it_IT/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Elimina piattaforma", "delete-platform-hint": "Rimuove la piattaforma e le voci di database delle sue ROM. I file su disco NON vengono eliminati.", "description": "Descrizione", + "developer": "Developer", "download-platform": "Scarica piattaforma", "edit-platform": "Modifica piattaforma", "export": "Esporta", @@ -48,6 +49,7 @@ "old-squared-cases": "Vecchie custodie quadrate", "on-disk": "Su disco", "player-count": "Numero di giocatori", + "publisher": "Publisher", "random-rom": "ROM casuale", "random-rom-empty": "Nessuna ROM in questa piattaforma", "random-rom-error": "Impossibile scegliere una ROM casuale", diff --git a/frontend/src/locales/it_IT/rom.json b/frontend/src/locales/it_IT/rom.json index 77bb00c545..eab3cddf5b 100644 --- a/frontend/src/locales/it_IT/rom.json +++ b/frontend/src/locales/it_IT/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM eliminata dal database | {count} ROM eliminate dal database", "deleted-from-filesystem": "{count} ROM eliminata dal filesystem | {count} ROM eliminate dal filesystem", "details": "Dettagli", + "developers": "Developers", "difficulty": "Difficoltà", "disc": "Disc", "download": "Scarica", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "ID SteamGridDB", "public": "Pubblica", "public-notes": "Note pubbliche", + "publishers": "Publishers", "qr-scan-to-download": "Scansiona per scaricare", "rating": "Valutazione", "redownload": "Riscarica", diff --git a/frontend/src/locales/it_IT/settings.json b/frontend/src/locales/it_IT/settings.json index 104c9edbf1..cb5d122c0b 100644 --- a/frontend/src/locales/it_IT/settings.json +++ b/frontend/src/locales/it_IT/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Cartella variante", "vc-all": "Tutto (lento)", "vc-collection": "Collezione", - "vc-company": "Sviluppatore", + "vc-company": "Compagnia", + "vc-developer": "Developer", "vc-franchise": "Franchise", "vc-genre": "Genere", "vc-mode": "Modalità di gioco", + "vc-publisher": "Publisher", "virtual-collection-type": "Tipo di collezione virtuale (Metodo di raggruppamento delle rom)", "watcher": "Monitor", "widget-library-stats": "Statistiche della libreria", diff --git a/frontend/src/locales/ja_JP/platform.json b/frontend/src/locales/ja_JP/platform.json index a133c1ae8d..cdd10caed8 100644 --- a/frontend/src/locales/ja_JP/platform.json +++ b/frontend/src/locales/ja_JP/platform.json @@ -13,6 +13,7 @@ "delete-platform": "プラットフォームの削除", "delete-platform-hint": "プラットフォームとそのROMデータベースエントリを削除します。ディスク上のファイルは削除されません。", "description": "説明", + "developer": "Developer", "download-platform": "プラットフォームをダウンロード", "edit-platform": "プラットフォームを編集", "export": "エクスポート", @@ -48,6 +49,7 @@ "old-squared-cases": "旧角型ケース", "on-disk": "ディスク上", "player-count": "プレイヤー", + "publisher": "Publisher", "random-rom": "ランダムROM", "random-rom-empty": "このプラットフォームにROMがありません", "random-rom-error": "ランダムなROMを選択できませんでした", diff --git a/frontend/src/locales/ja_JP/rom.json b/frontend/src/locales/ja_JP/rom.json index 45794e646e..fc9b4e8643 100644 --- a/frontend/src/locales/ja_JP/rom.json +++ b/frontend/src/locales/ja_JP/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count}件のROMがデータベースから削除されました", "deleted-from-filesystem": "{count}件のROMがファイルシステムから削除されました", "details": "詳細", + "developers": "Developers", "difficulty": "難易度", "disc": "Disc", "download": "ダウンロード", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "公開", "public-notes": "公開メモ", + "publishers": "Publishers", "qr-scan-to-download": "スキャンしてダウンロード", "rating": "レーティング", "redownload": "再ダウンロード", diff --git a/frontend/src/locales/ja_JP/settings.json b/frontend/src/locales/ja_JP/settings.json index f61fccd6a7..1e470a2d88 100644 --- a/frontend/src/locales/ja_JP/settings.json +++ b/frontend/src/locales/ja_JP/settings.json @@ -474,10 +474,12 @@ "variant-folder": "バリアントフォルダ", "vc-all": "すべて(低速)", "vc-collection": "コレクション", - "vc-company": "デベロッパー", + "vc-company": "会社", + "vc-developer": "Developer", "vc-franchise": "フランチャイズ", "vc-genre": "ジャンル", "vc-mode": "プレイモード", + "vc-publisher": "Publisher", "virtual-collection-type": "バーチャルコレクションタイプ (romのグループ化方法)", "watcher": "ウォッチャー", "widget-library-stats": "ライブラリ統計", diff --git a/frontend/src/locales/ko_KR/platform.json b/frontend/src/locales/ko_KR/platform.json index 3637b0ee25..4342487676 100644 --- a/frontend/src/locales/ko_KR/platform.json +++ b/frontend/src/locales/ko_KR/platform.json @@ -13,6 +13,7 @@ "delete-platform": "플랫폼 지우기", "delete-platform-hint": "플랫폼과 해당 ROM 데이터베이스 항목을 제거합니다. 디스크의 파일은 삭제되지 않습니다.", "description": "설명", + "developer": "Developer", "download-platform": "플랫폼 다운로드", "edit-platform": "플랫폼 편집", "export": "내보내기", @@ -48,6 +49,7 @@ "old-squared-cases": "옛 정사각형 케이스", "on-disk": "디스크에 있음", "player-count": "플레이어 수", + "publisher": "Publisher", "random-rom": "무작위 ROM", "random-rom-empty": "이 플랫폼에 ROM이 없습니다", "random-rom-error": "무작위 ROM을 선택할 수 없습니다", diff --git a/frontend/src/locales/ko_KR/rom.json b/frontend/src/locales/ko_KR/rom.json index ba7bf8ff87..b4fe795828 100644 --- a/frontend/src/locales/ko_KR/rom.json +++ b/frontend/src/locales/ko_KR/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count}개의 ROM이 데이터베이스에서 삭제되었습니다", "deleted-from-filesystem": "{count}개의 ROM이 파일 시스템에서 삭제되었습니다", "details": "자세히", + "developers": "Developers", "difficulty": "난이도", "disc": "Disc", "download": "다운로드", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "공개", "public-notes": "공개 노트", + "publishers": "Publishers", "qr-scan-to-download": "스캔하여 다운로드", "rating": "평가", "redownload": "다시 다운로드", diff --git a/frontend/src/locales/ko_KR/settings.json b/frontend/src/locales/ko_KR/settings.json index 8c152efe32..2da1efe622 100644 --- a/frontend/src/locales/ko_KR/settings.json +++ b/frontend/src/locales/ko_KR/settings.json @@ -475,9 +475,11 @@ "vc-all": "전체 (느림)", "vc-collection": "컬렉션", "vc-company": "개발사", + "vc-developer": "Developer", "vc-franchise": "프랜차이즈", "vc-genre": "장르", "vc-mode": "플레이 모드", + "vc-publisher": "Publisher", "virtual-collection-type": "자동 생성된 모음집 유형(롬 그룹화 방법)", "watcher": "감시자", "widget-library-stats": "라이브러리 통계", diff --git a/frontend/src/locales/pl_PL/platform.json b/frontend/src/locales/pl_PL/platform.json index 8db9ae8361..a380f236f1 100644 --- a/frontend/src/locales/pl_PL/platform.json +++ b/frontend/src/locales/pl_PL/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Usuń platformę", "delete-platform-hint": "Usuwa platformę i wpisy ROM-ów z bazy danych. Pliki na dysku NIE zostaną usunięte.", "description": "Opis", + "developer": "Developer", "download-platform": "Pobierz platforme", "edit-platform": "Edytuj platformę", "export": "Eksportuj", @@ -48,6 +49,7 @@ "old-squared-cases": "Stare pudełka kwadratowe", "on-disk": "Na dysku", "player-count": "Liczba graczy", + "publisher": "Publisher", "random-rom": "Losowy ROM", "random-rom-empty": "Brak ROM-ów na tej platformie", "random-rom-error": "Nie udało się wybrać losowego ROM-u", diff --git a/frontend/src/locales/pl_PL/rom.json b/frontend/src/locales/pl_PL/rom.json index 343d3731b8..ef5f10586e 100644 --- a/frontend/src/locales/pl_PL/rom.json +++ b/frontend/src/locales/pl_PL/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM usunięty z bazy danych | {count} ROM-ów usuniętych z bazy danych", "deleted-from-filesystem": "{count} ROM usunięty z systemu plików | {count} ROM-ów usuniętych z systemu plików", "details": "Szczegóły", + "developers": "Developers", "difficulty": "Poziom trudności", "disc": "Disc", "download": "Pobierz", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Publiczna", "public-notes": "Notatki publiczne", + "publishers": "Publishers", "qr-scan-to-download": "Zeskanuj, aby pobrać", "rating": "Ocena", "redownload": "Pobierz ponownie", diff --git a/frontend/src/locales/pl_PL/settings.json b/frontend/src/locales/pl_PL/settings.json index 21c5b12856..19671128a2 100644 --- a/frontend/src/locales/pl_PL/settings.json +++ b/frontend/src/locales/pl_PL/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Folder wariantu", "vc-all": "Wszystkie (powoli)", "vc-collection": "Kolekcja", - "vc-company": "Deweloper", + "vc-company": "Firma", + "vc-developer": "Developer", "vc-franchise": "Seria", "vc-genre": "Gatunek", "vc-mode": "Tryb gry", + "vc-publisher": "Publisher", "virtual-collection-type": "Typ automatycznej kolekcji (metoda grupowania ROM-ów)", "watcher": "Monitor", "widget-library-stats": "Statystyki biblioteki", diff --git a/frontend/src/locales/pt_BR/platform.json b/frontend/src/locales/pt_BR/platform.json index 5cdf6c894c..9e03243fd8 100644 --- a/frontend/src/locales/pt_BR/platform.json +++ b/frontend/src/locales/pt_BR/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Excluir plataforma", "delete-platform-hint": "Remove a plataforma e seus registros de ROM no banco de dados. Os arquivos no disco NÃO são excluídos.", "description": "Descrição", + "developer": "Developer", "download-platform": "Baixar plataforma", "edit-platform": "Editar plataforma", "export": "Exportar", @@ -48,6 +49,7 @@ "old-squared-cases": "Caixas quadradas antigas", "on-disk": "No disco", "player-count": "Niver ar c'hoarierien", + "publisher": "Publisher", "random-rom": "ROM aleatório", "random-rom-empty": "Nenhuma ROM nesta plataforma", "random-rom-error": "Não foi possível escolher uma ROM aleatória", diff --git a/frontend/src/locales/pt_BR/rom.json b/frontend/src/locales/pt_BR/rom.json index 2636479449..b9d1ae682f 100644 --- a/frontend/src/locales/pt_BR/rom.json +++ b/frontend/src/locales/pt_BR/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM excluída do banco de dados | {count} ROMs excluídas do banco de dados", "deleted-from-filesystem": "{count} ROM excluída do sistema de arquivos | {count} ROMs excluídas do sistema de arquivos", "details": "Detalhes", + "developers": "Developers", "difficulty": "Dificuldade", "disc": "Disc", "download": "Baixar", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "ID do SteamGridDB", "public": "Pública", "public-notes": "Notas públicas", + "publishers": "Publishers", "qr-scan-to-download": "Escaneie para baixar", "rating": "Avaliação", "redownload": "Baixar novamente", diff --git a/frontend/src/locales/pt_BR/settings.json b/frontend/src/locales/pt_BR/settings.json index 60fb7da99c..c59da259c1 100644 --- a/frontend/src/locales/pt_BR/settings.json +++ b/frontend/src/locales/pt_BR/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Pasta variante", "vc-all": "Todos (lento)", "vc-collection": "Coleção", - "vc-company": "Desenvolvedora", + "vc-company": "Empresa", + "vc-developer": "Developer", "vc-franchise": "Franquia", "vc-genre": "Gênero", "vc-mode": "Modo de jogo", + "vc-publisher": "Publisher", "virtual-collection-type": "Tipo de coleção virtual (método de agrupamento de ROM)", "watcher": "Monitor", "widget-library-stats": "Estatísticas da biblioteca", diff --git a/frontend/src/locales/ro_RO/platform.json b/frontend/src/locales/ro_RO/platform.json index aa56b62095..92fd1adb7b 100644 --- a/frontend/src/locales/ro_RO/platform.json +++ b/frontend/src/locales/ro_RO/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Șterge platforma", "delete-platform-hint": "Elimină platforma și intrările ROM din baza de date. Fișierele de pe disc NU sunt șterse.", "description": "Descriere", + "developer": "Developer", "download-platform": "Descarcă platforma", "edit-platform": "Editează platforma", "export": "Exportare", @@ -48,6 +49,7 @@ "old-squared-cases": "Cutii pătrate vechi", "on-disk": "Pe disc", "player-count": "Numărul de jucători", + "publisher": "Publisher", "random-rom": "ROM aleatoriu", "random-rom-empty": "Niciun ROM pe această platformă", "random-rom-error": "Nu s-a putut alege un ROM aleatoriu", diff --git a/frontend/src/locales/ro_RO/rom.json b/frontend/src/locales/ro_RO/rom.json index fdc4c864a0..b19f0a09c9 100644 --- a/frontend/src/locales/ro_RO/rom.json +++ b/frontend/src/locales/ro_RO/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM șters din baza de date | {count} ROM-uri șterse din baza de date", "deleted-from-filesystem": "{count} ROM șters din sistemul de fișiere | {count} ROM-uri șterse din sistemul de fișiere", "details": "Detalii", + "developers": "Developers", "difficulty": "Dificultate", "disc": "Disc", "download": "Descarcă", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "ID SteamGridDB", "public": "Publică", "public-notes": "Notițe publice", + "publishers": "Publishers", "qr-scan-to-download": "Scanează pentru a descărca", "rating": "Evaluare", "redownload": "Redescarcă", diff --git a/frontend/src/locales/ro_RO/settings.json b/frontend/src/locales/ro_RO/settings.json index 4c736da833..c89f1a41ea 100644 --- a/frontend/src/locales/ro_RO/settings.json +++ b/frontend/src/locales/ro_RO/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Dosar variantă", "vc-all": "Toate (lent)", "vc-collection": "Colecție", - "vc-company": "Dezvoltator", + "vc-company": "Companie", + "vc-developer": "Developer", "vc-franchise": "Franciză", "vc-genre": "Gen", "vc-mode": "Mod de joc", + "vc-publisher": "Publisher", "virtual-collection-type": "Tip de colecție virtuală (metoda de grupare a rom-urilor)", "watcher": "Monitor", "widget-library-stats": "Statistici bibliotecă", diff --git a/frontend/src/locales/ru_RU/platform.json b/frontend/src/locales/ru_RU/platform.json index d8ed42a62f..253194c6d1 100644 --- a/frontend/src/locales/ru_RU/platform.json +++ b/frontend/src/locales/ru_RU/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Удалить платформу", "delete-platform-hint": "Удаляет платформу и её записи о ROM из базы. Файлы на диске НЕ удаляются.", "description": "Описание", + "developer": "Developer", "download-platform": "Скачать платформу", "edit-platform": "Редактировать платформу", "export": "Экспорт", @@ -48,6 +49,7 @@ "old-squared-cases": "Старые квадратные кейсы", "on-disk": "На диске", "player-count": "Количество игроков", + "publisher": "Publisher", "random-rom": "Случайный ROM", "random-rom-empty": "В этой платформе нет ROM", "random-rom-error": "Не удалось выбрать случайный ROM", diff --git a/frontend/src/locales/ru_RU/rom.json b/frontend/src/locales/ru_RU/rom.json index f2740cb7b9..328605c058 100644 --- a/frontend/src/locales/ru_RU/rom.json +++ b/frontend/src/locales/ru_RU/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM удалён из базы данных | {count} ROM-ов удалено из базы данных", "deleted-from-filesystem": "{count} ROM удалён из файловой системы | {count} ROM-ов удалено из файловой системы", "details": "Детали", + "developers": "Developers", "difficulty": "Сложность", "disc": "Disc", "download": "Скачать", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Публичная", "public-notes": "Публичные заметки", + "publishers": "Publishers", "qr-scan-to-download": "Отсканируйте для скачивания", "rating": "Рейтинг", "redownload": "Перезагрузить", diff --git a/frontend/src/locales/ru_RU/settings.json b/frontend/src/locales/ru_RU/settings.json index edc93b8a7d..4adf2286a7 100644 --- a/frontend/src/locales/ru_RU/settings.json +++ b/frontend/src/locales/ru_RU/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Папка варианта", "vc-all": "Все (медленно)", "vc-collection": "Коллекция", - "vc-company": "Разработчик", + "vc-company": "Компания", + "vc-developer": "Developer", "vc-franchise": "Франшиза", "vc-genre": "Жанр", "vc-mode": "Режим игры", + "vc-publisher": "Publisher", "virtual-collection-type": "Тип виртуальной коллекции (метод группировки ромов)", "watcher": "Наблюдатель", "widget-library-stats": "Статистика библиотеки", diff --git a/frontend/src/locales/tr_TR/platform.json b/frontend/src/locales/tr_TR/platform.json index 5a6e7f4529..b6812ce455 100644 --- a/frontend/src/locales/tr_TR/platform.json +++ b/frontend/src/locales/tr_TR/platform.json @@ -13,6 +13,7 @@ "delete-platform": "Platformu sil", "delete-platform-hint": "Platformu ve ROM veritabanı kayıtlarını kaldırır. Diskteki dosyalar SİLİNMEZ.", "description": "Açıklama", + "developer": "Developer", "download-platform": "Platformu indir", "edit-platform": "Platformu düzenle", "export": "Dışa aktar", @@ -48,6 +49,7 @@ "old-squared-cases": "Eski kare kutular", "on-disk": "Diskte", "player-count": "Oyuncu sayısı", + "publisher": "Publisher", "random-rom": "Rastgele ROM", "random-rom-empty": "Bu platformda ROM yok", "random-rom-error": "Rastgele ROM seçilemedi", diff --git a/frontend/src/locales/tr_TR/rom.json b/frontend/src/locales/tr_TR/rom.json index e8c0b64b06..55b1666e76 100644 --- a/frontend/src/locales/tr_TR/rom.json +++ b/frontend/src/locales/tr_TR/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} ROM veritabanından silindi | {count} ROM veritabanından silindi", "deleted-from-filesystem": "{count} ROM dosya sisteminden silindi | {count} ROM dosya sisteminden silindi", "details": "Ayrıntılar", + "developers": "Developers", "difficulty": "Zorluk", "disc": "Disk", "download": "İndir", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Herkese açık", "public-notes": "Herkese açık notlar", + "publishers": "Publishers", "qr-scan-to-download": "İndirmek için tarat", "rating": "Puan", "redownload": "Yeniden indir", diff --git a/frontend/src/locales/tr_TR/settings.json b/frontend/src/locales/tr_TR/settings.json index ba36c31e5e..84c7531d6b 100644 --- a/frontend/src/locales/tr_TR/settings.json +++ b/frontend/src/locales/tr_TR/settings.json @@ -474,10 +474,12 @@ "variant-folder": "Varyant klasörü", "vc-all": "Tümü (yavaş)", "vc-collection": "Koleksiyonu", - "vc-company": "Geliştirici", + "vc-company": "Şirket", + "vc-developer": "Developer", "vc-franchise": "Franchise", "vc-genre": "Tür", "vc-mode": "Oyun Modu", + "vc-publisher": "Publisher", "virtual-collection-type": "Sanal koleksiyon türü (ROM gruplama yöntemi)", "watcher": "İzleyici", "widget-library-stats": "Kütüphane istatistikleri", diff --git a/frontend/src/locales/zh_CN/platform.json b/frontend/src/locales/zh_CN/platform.json index f73ad27566..1b6632fce3 100644 --- a/frontend/src/locales/zh_CN/platform.json +++ b/frontend/src/locales/zh_CN/platform.json @@ -13,6 +13,7 @@ "delete-platform": "删除平台", "delete-platform-hint": "移除该平台及其 ROM 数据库条目。磁盘上的文件不会被删除。", "description": "描述", + "developer": "Developer", "download-platform": "下载平台", "edit-platform": "编辑平台", "export": "导出", @@ -48,6 +49,7 @@ "old-squared-cases": "旧式方形盒装", "on-disk": "磁盘上", "player-count": "玩家人数", + "publisher": "Publisher", "random-rom": "随机 ROM", "random-rom-empty": "此平台没有 ROM", "random-rom-error": "无法选择随机 ROM", diff --git a/frontend/src/locales/zh_CN/rom.json b/frontend/src/locales/zh_CN/rom.json index 299338d37c..f9cf2946bc 100644 --- a/frontend/src/locales/zh_CN/rom.json +++ b/frontend/src/locales/zh_CN/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} 个 ROM 已从数据库删除", "deleted-from-filesystem": "{count} 个 ROM 已从文件系统删除", "details": "详情", + "developers": "Developers", "difficulty": "难度", "disc": "Disc", "download": "下载", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "公开", "public-notes": "公共笔记", + "publishers": "Publishers", "qr-scan-to-download": "扫码下载", "rating": "评分", "redownload": "重新下载", diff --git a/frontend/src/locales/zh_CN/settings.json b/frontend/src/locales/zh_CN/settings.json index 28725d03a2..f8644e863f 100644 --- a/frontend/src/locales/zh_CN/settings.json +++ b/frontend/src/locales/zh_CN/settings.json @@ -474,10 +474,12 @@ "variant-folder": "变体文件夹", "vc-all": "全部(较慢)", "vc-collection": "收藏", - "vc-company": "开发商", + "vc-company": "公司", + "vc-developer": "Developer", "vc-franchise": "系列", "vc-genre": "类型", "vc-mode": "游玩模式", + "vc-publisher": "Publisher", "virtual-collection-type": "虚拟收藏类型(Rom 文件分组方法)", "watcher": "监视器", "widget-library-stats": "库统计", diff --git a/frontend/src/locales/zh_TW/platform.json b/frontend/src/locales/zh_TW/platform.json index e988d30d4c..b2384c0b08 100644 --- a/frontend/src/locales/zh_TW/platform.json +++ b/frontend/src/locales/zh_TW/platform.json @@ -13,6 +13,7 @@ "delete-platform": "删除平台", "delete-platform-hint": "移除此平台及其 ROM 資料庫項目。磁碟上的檔案不會被刪除。", "description": "說明", + "developer": "Developer", "download-platform": "下載平台", "edit-platform": "編輯平台", "export": "導出", @@ -48,6 +49,7 @@ "old-squared-cases": "舊式方形卡帶盒", "on-disk": "在磁碟上", "player-count": "玩家人數", + "publisher": "Publisher", "random-rom": "隨機 ROM", "random-rom-empty": "此平台沒有 ROM", "random-rom-error": "無法選擇隨機 ROM", diff --git a/frontend/src/locales/zh_TW/rom.json b/frontend/src/locales/zh_TW/rom.json index 33f6e1a37a..e6aa4643fc 100644 --- a/frontend/src/locales/zh_TW/rom.json +++ b/frontend/src/locales/zh_TW/rom.json @@ -114,6 +114,7 @@ "deleted-from-database": "{count} 個 ROM 已從資料庫刪除", "deleted-from-filesystem": "{count} 個 ROM 已從檔案系統刪除", "details": "詳情", + "developers": "Developers", "difficulty": "難度", "disc": "Disc", "download": "下載", @@ -291,6 +292,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "公開", "public-notes": "公共筆記", + "publishers": "Publishers", "qr-scan-to-download": "掃描以下載", "rating": "評分", "redownload": "重新下載", diff --git a/frontend/src/locales/zh_TW/settings.json b/frontend/src/locales/zh_TW/settings.json index b6ae7c3683..a1fce5bddb 100644 --- a/frontend/src/locales/zh_TW/settings.json +++ b/frontend/src/locales/zh_TW/settings.json @@ -474,10 +474,12 @@ "variant-folder": "變體資料夾", "vc-all": "全部(緩慢)", "vc-collection": "收藏", - "vc-company": "開發商", + "vc-company": "公司", + "vc-developer": "Developer", "vc-franchise": "系列", "vc-genre": "類型", "vc-mode": "遊玩模式", + "vc-publisher": "Publisher", "virtual-collection-type": "自動生成收藏庫形式(Rom 分組方式)", "watcher": "監視器", "widget-library-stats": "庫統計", diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index 33e1dba52f..f384967de6 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -164,6 +164,8 @@ export interface GetRomsParams { selectedFranchises?: string[] | null; selectedCollections?: string[] | null; selectedCompanies?: string[] | null; + selectedPublishers?: string[] | null; + selectedDevelopers?: string[] | null; selectedAgeRatings?: string[] | null; selectedRegions?: string[] | null; selectedLanguages?: string[] | null; @@ -176,6 +178,8 @@ export interface GetRomsParams { franchisesLogic?: string | null; collectionsLogic?: string | null; companiesLogic?: string | null; + publishersLogic?: string | null; + developersLogic?: string | null; ageRatingsLogic?: string | null; regionsLogic?: string | null; languagesLogic?: string | null; @@ -216,6 +220,8 @@ async function getRoms({ selectedFranchises = null, selectedCollections = null, selectedCompanies = null, + selectedPublishers = null, + selectedDevelopers = null, selectedAgeRatings = null, selectedRegions = null, selectedLanguages = null, @@ -228,6 +234,8 @@ async function getRoms({ franchisesLogic = null, collectionsLogic = null, companiesLogic = null, + publishersLogic = null, + developersLogic = null, ageRatingsLogic = null, regionsLogic = null, languagesLogic = null, @@ -266,6 +274,14 @@ async function getRoms({ selectedCompanies && selectedCompanies.length > 0 ? selectedCompanies : undefined, + publishers: + selectedPublishers && selectedPublishers.length > 0 + ? selectedPublishers + : undefined, + developers: + selectedDevelopers && selectedDevelopers.length > 0 + ? selectedDevelopers + : undefined, age_ratings: selectedAgeRatings && selectedAgeRatings.length > 0 ? selectedAgeRatings @@ -308,6 +324,14 @@ async function getRoms({ selectedCompanies && selectedCompanies.length > 0 ? companiesLogic || "any" : undefined, + publishers_logic: + selectedPublishers && selectedPublishers.length > 0 + ? publishersLogic || "any" + : undefined, + developers_logic: + selectedDevelopers && selectedDevelopers.length > 0 + ? developersLogic || "any" + : undefined, age_ratings_logic: selectedAgeRatings && selectedAgeRatings.length > 0 ? ageRatingsLogic || "any" diff --git a/frontend/src/stores/galleryFilter.ts b/frontend/src/stores/galleryFilter.ts index 25deb67967..cc3d48be9b 100644 --- a/frontend/src/stores/galleryFilter.ts +++ b/frontend/src/stores/galleryFilter.ts @@ -7,6 +7,8 @@ export type FilterType = | "franchises" | "collections" | "companies" + | "publishers" + | "developers" | "ageRatings" | "statuses" | "regions" @@ -30,6 +32,8 @@ const buildDefaultFilterState = () => ({ filterFranchises: [] as string[], filterCollections: [] as string[], filterCompanies: [] as string[], + filterPublishers: [] as string[], + filterDevelopers: [] as string[], filterAgeRatings: [] as string[], filterRegions: [] as string[], filterLanguages: [] as string[], @@ -52,6 +56,8 @@ const buildDefaultFilterState = () => ({ selectedFranchises: [] as string[], selectedCollections: [] as string[], selectedCompanies: [] as string[], + selectedPublishers: [] as string[], + selectedDevelopers: [] as string[], selectedAgeRatings: [] as string[], selectedRegions: [] as string[], selectedLanguages: [] as string[], @@ -64,6 +70,8 @@ const buildDefaultFilterState = () => ({ franchisesLogic: "any" as FilterLogicOperator, collectionsLogic: "any" as FilterLogicOperator, companiesLogic: "any" as FilterLogicOperator, + publishersLogic: "any" as FilterLogicOperator, + developersLogic: "any" as FilterLogicOperator, ageRatingsLogic: "any" as FilterLogicOperator, regionsLogic: "any" as FilterLogicOperator, languagesLogic: "any" as FilterLogicOperator, @@ -95,6 +103,12 @@ export default defineStore("galleryFilter", { setFilterCompanies(companies: string[]) { this.filterCompanies = companies; }, + setFilterPublishers(publishers: string[]) { + this.filterPublishers = publishers; + }, + setFilterDevelopers(developers: string[]) { + this.filterDevelopers = developers; + }, setFilterAgeRatings(ageRatings: string[]) { this.filterAgeRatings = ageRatings; }, @@ -144,6 +158,18 @@ export default defineStore("galleryFilter", { setCompaniesLogic(logic: FilterLogicOperator) { this.companiesLogic = logic; }, + setSelectedFilterPublishers(publishers: string[]) { + this.selectedPublishers = publishers; + }, + setPublishersLogic(logic: FilterLogicOperator) { + this.publishersLogic = logic; + }, + setSelectedFilterDevelopers(developers: string[]) { + this.selectedDevelopers = developers; + }, + setDevelopersLogic(logic: FilterLogicOperator) { + this.developersLogic = logic; + }, setSelectedFilterAgeRatings(ageRatings: string[]) { this.selectedAgeRatings = ageRatings; }, @@ -432,6 +458,8 @@ export default defineStore("galleryFilter", { this.selectedFranchises.length > 0 || this.selectedCollections.length > 0 || this.selectedCompanies.length > 0 || + this.selectedPublishers.length > 0 || + this.selectedDevelopers.length > 0 || this.selectedAgeRatings.length > 0 || this.selectedRegions.length > 0 || this.selectedLanguages.length > 0 || @@ -451,6 +479,8 @@ export default defineStore("galleryFilter", { this.selectedFranchises = []; this.selectedCollections = []; this.selectedCompanies = []; + this.selectedPublishers = []; + this.selectedDevelopers = []; this.selectedAgeRatings = []; this.selectedRegions = []; this.selectedLanguages = []; @@ -473,6 +503,8 @@ export default defineStore("galleryFilter", { this.franchisesLogic = "any"; this.collectionsLogic = "any"; this.companiesLogic = "any"; + this.publishersLogic = "any"; + this.developersLogic = "any"; this.ageRatingsLogic = "any"; this.regionsLogic = "any"; this.languagesLogic = "any"; diff --git a/frontend/src/v2/components/Dialogs/CreateSmartCollectionDialog.vue b/frontend/src/v2/components/Dialogs/CreateSmartCollectionDialog.vue index bb0296510f..b0a8843012 100644 --- a/frontend/src/v2/components/Dialogs/CreateSmartCollectionDialog.vue +++ b/frontend/src/v2/components/Dialogs/CreateSmartCollectionDialog.vue @@ -101,6 +101,10 @@ const openHandler = () => { collectionsLogic: galleryFilter.collectionsLogic, selectedCompanies: galleryFilter.selectedCompanies, companiesLogic: galleryFilter.companiesLogic, + selectedPublishers: galleryFilter.selectedPublishers, + publishersLogic: galleryFilter.publishersLogic, + selectedDevelopers: galleryFilter.selectedDevelopers, + developersLogic: galleryFilter.developersLogic, selectedAgeRatings: galleryFilter.selectedAgeRatings, ageRatingsLogic: galleryFilter.ageRatingsLogic, selectedRegions: galleryFilter.selectedRegions, diff --git a/frontend/src/v2/components/EditRom/AdditionalDetails.vue b/frontend/src/v2/components/EditRom/AdditionalDetails.vue index 4a021ccab1..5968c362b6 100644 --- a/frontend/src/v2/components/EditRom/AdditionalDetails.vue +++ b/frontend/src/v2/components/EditRom/AdditionalDetails.vue @@ -85,6 +85,24 @@ function updateManualMetadata( clearable @update:model-value="(v) => updateManualMetadata('companies', v)" /> + + (() => [ logic: companiesLogic, setLogic: (l) => filter.setCompaniesLogic(l), }, + { + label: t("platform.publisher"), + icon: "mdi-bank-outline", + items: filterPublishers, + selected: selectedPublishers, + logic: publishersLogic, + setLogic: (l) => filter.setPublishersLogic(l), + }, + { + label: t("platform.developer"), + icon: "mdi-code-tags", + items: filterDevelopers, + selected: selectedDevelopers, + logic: developersLogic, + setLogic: (l) => filter.setDevelopersLogic(l), + }, { label: t("platform.age-rating"), icon: "mdi-account-child", diff --git a/frontend/src/v2/components/Gallery/GalleryShell.vue b/frontend/src/v2/components/Gallery/GalleryShell.vue index 288f9436f2..c77e724404 100644 --- a/frontend/src/v2/components/Gallery/GalleryShell.vue +++ b/frontend/src/v2/components/Gallery/GalleryShell.vue @@ -166,6 +166,8 @@ const { selectedFranchises, selectedCollections, selectedCompanies, + selectedPublishers, + selectedDevelopers, selectedAgeRatings, selectedRegions, selectedLanguages, @@ -177,6 +179,8 @@ const { franchisesLogic, collectionsLogic, companiesLogic, + publishersLogic, + developersLogic, ageRatingsLogic, regionsLogic, languagesLogic, @@ -211,6 +215,8 @@ const filterActiveCount = computed(() => { selectedFranchises, selectedCollections, selectedCompanies, + selectedPublishers, + selectedDevelopers, selectedAgeRatings, selectedRegions, selectedLanguages, @@ -246,6 +252,8 @@ watch( selectedFranchises, selectedCollections, selectedCompanies, + selectedPublishers, + selectedDevelopers, selectedAgeRatings, selectedRegions, selectedLanguages, @@ -257,6 +265,8 @@ watch( franchisesLogic, collectionsLogic, companiesLogic, + publishersLogic, + developersLogic, ageRatingsLogic, regionsLogic, languagesLogic, diff --git a/frontend/src/v2/components/GameDetails/OverviewTab.vue b/frontend/src/v2/components/GameDetails/OverviewTab.vue index c0bbfc761d..37dc5fabae 100644 --- a/frontend/src/v2/components/GameDetails/OverviewTab.vue +++ b/frontend/src/v2/components/GameDetails/OverviewTab.vue @@ -7,8 +7,9 @@ // characteristics, rendered as semantic badges rather than chips) // 4. RomM Collections — the user's personal collections this ROM // lives in, rendered as bookmark-icon chip RouterLinks -// 5. Info grid (Genres / Companies / Franchises / Collections — -// "Companies" is the API field for merged developer + publisher) +// 5. Info grid (Genres / Developers / Publishers / Franchises / +// Collections — the merged "Companies" list is only shown as a +// fallback when the publisher/developer split is empty) // 6. Screenshots (also reachable via the Media tab's Screenshots subtab, // which is where uploads will live) // 7. HLTB strip diff --git a/frontend/src/v2/components/GameDetails/RelatedGameCard.vue b/frontend/src/v2/components/GameDetails/RelatedGameCard.vue index 6a98289ccf..a65754d56e 100644 --- a/frontend/src/v2/components/GameDetails/RelatedGameCard.vue +++ b/frontend/src/v2/components/GameDetails/RelatedGameCard.vue @@ -60,6 +60,8 @@ const EMPTY_METADATA: RomMetadataSchema = { franchises: [], collections: [], companies: [], + publishers: [], + developers: [], game_modes: [], age_ratings: [], player_count: "", diff --git a/frontend/src/v2/composables/useGalleryFilterUrl/index.ts b/frontend/src/v2/composables/useGalleryFilterUrl/index.ts index 2e79553d13..981e1084c5 100644 --- a/frontend/src/v2/composables/useGalleryFilterUrl/index.ts +++ b/frontend/src/v2/composables/useGalleryFilterUrl/index.ts @@ -25,6 +25,8 @@ // ?franchises=…&franchisesLogic=… // ?collections=…&collectionsLogic=… // ?companies=…&companiesLogic=… +// ?publishers=…&publishersLogic=… +// ?developers=…&developersLogic=… // ?ageRatings=…&ageRatingsLogic=… // ?regions=…®ionsLogic=… // ?languages=…&languagesLogic=… @@ -114,6 +116,8 @@ export function useGalleryFilterUrl() { selectedFranchises, selectedCollections, selectedCompanies, + selectedPublishers, + selectedDevelopers, selectedAgeRatings, selectedRegions, selectedLanguages, @@ -125,6 +129,8 @@ export function useGalleryFilterUrl() { franchisesLogic, collectionsLogic, companiesLogic, + publishersLogic, + developersLogic, ageRatingsLogic, regionsLogic, languagesLogic, @@ -164,6 +170,10 @@ export function useGalleryFilterUrl() { collectionsLogic: qLogic(q.collectionsLogic), companies: qList(q.companies), companiesLogic: qLogic(q.companiesLogic), + publishers: qList(q.publishers), + publishersLogic: qLogic(q.publishersLogic), + developers: qList(q.developers), + developersLogic: qLogic(q.developersLogic), ageRatings: qList(q.ageRatings), ageRatingsLogic: qLogic(q.ageRatingsLogic), regions: qList(q.regions), @@ -240,6 +250,16 @@ export function useGalleryFilterUrl() { if (url.companiesLogic && url.companiesLogic !== companiesLogic.value) filter.setCompaniesLogic(url.companiesLogic); + if (!eqStrArr(url.publishers, selectedPublishers.value)) + filter.setSelectedFilterPublishers(url.publishers); + if (url.publishersLogic && url.publishersLogic !== publishersLogic.value) + filter.setPublishersLogic(url.publishersLogic); + + if (!eqStrArr(url.developers, selectedDevelopers.value)) + filter.setSelectedFilterDevelopers(url.developers); + if (url.developersLogic && url.developersLogic !== developersLogic.value) + filter.setDevelopersLogic(url.developersLogic); + if (!eqStrArr(url.ageRatings, selectedAgeRatings.value)) filter.setSelectedFilterAgeRatings(url.ageRatings); if (url.ageRatingsLogic && url.ageRatingsLogic !== ageRatingsLogic.value) @@ -360,6 +380,16 @@ export function useGalleryFilterUrl() { "companiesLogic", selectedCompanies.value.length > 0 ? companiesLogic.value : null, ); + setList("publishers", selectedPublishers.value); + setOrDelete( + "publishersLogic", + selectedPublishers.value.length > 0 ? publishersLogic.value : null, + ); + setList("developers", selectedDevelopers.value); + setOrDelete( + "developersLogic", + selectedDevelopers.value.length > 0 ? developersLogic.value : null, + ); setList("ageRatings", selectedAgeRatings.value); setOrDelete( "ageRatingsLogic", @@ -440,6 +470,10 @@ export function useGalleryFilterUrl() { collectionsLogic, selectedCompanies, companiesLogic, + selectedPublishers, + publishersLogic, + selectedDevelopers, + developersLogic, selectedAgeRatings, ageRatingsLogic, selectedRegions, diff --git a/frontend/src/v2/stores/galleryRoms.ts b/frontend/src/v2/stores/galleryRoms.ts index fd8c371c21..975db59c56 100644 --- a/frontend/src/v2/stores/galleryRoms.ts +++ b/frontend/src/v2/stores/galleryRoms.ts @@ -351,6 +351,8 @@ export default defineStore("v2GalleryRoms", { selectedFranchises: galleryFilter.selectedFranchises, selectedCollections: galleryFilter.selectedCollections, selectedCompanies: galleryFilter.selectedCompanies, + selectedPublishers: galleryFilter.selectedPublishers, + selectedDevelopers: galleryFilter.selectedDevelopers, selectedAgeRatings: galleryFilter.selectedAgeRatings, selectedRegions: galleryFilter.selectedRegions, selectedLanguages: galleryFilter.selectedLanguages, @@ -362,6 +364,8 @@ export default defineStore("v2GalleryRoms", { franchisesLogic: galleryFilter.franchisesLogic, collectionsLogic: galleryFilter.collectionsLogic, companiesLogic: galleryFilter.companiesLogic, + publishersLogic: galleryFilter.publishersLogic, + developersLogic: galleryFilter.developersLogic, ageRatingsLogic: galleryFilter.ageRatingsLogic, regionsLogic: galleryFilter.regionsLogic, languagesLogic: galleryFilter.languagesLogic, @@ -398,6 +402,8 @@ export default defineStore("v2GalleryRoms", { galleryFilter.setFilterGenres(data.filter_values.genres); galleryFilter.setFilterFranchises(data.filter_values.franchises); galleryFilter.setFilterCompanies(data.filter_values.companies); + galleryFilter.setFilterPublishers(data.filter_values.publishers); + galleryFilter.setFilterDevelopers(data.filter_values.developers); galleryFilter.setFilterAgeRatings(data.filter_values.age_ratings); galleryFilter.setFilterRegions(data.filter_values.regions); galleryFilter.setFilterLanguages(data.filter_values.languages); diff --git a/frontend/src/v2/utils/smartCollectionCriteria.test.ts b/frontend/src/v2/utils/smartCollectionCriteria.test.ts index 2fc04aaa94..4d5544fb30 100644 --- a/frontend/src/v2/utils/smartCollectionCriteria.test.ts +++ b/frontend/src/v2/utils/smartCollectionCriteria.test.ts @@ -32,6 +32,10 @@ function emptySnapshot(): GalleryFilterSnapshot { collectionsLogic: "any", selectedCompanies: [], companiesLogic: "any", + selectedPublishers: [], + publishersLogic: "any", + selectedDevelopers: [], + developersLogic: "any", selectedAgeRatings: [], ageRatingsLogic: "any", selectedRegions: [], @@ -81,6 +85,47 @@ describe("buildSmartFilterCriteria — metadata providers", () => { }); }); +describe("buildSmartFilterCriteria — publishers and developers", () => { + it("serializes publishers and developers with their logic operators", () => { + const out = buildSmartFilterCriteria({ + ...emptySnapshot(), + selectedPublishers: ["Atari"], + publishersLogic: "any", + selectedDevelopers: ["Artech Studios"], + developersLogic: "all", + }); + + expect(out.publishers).toEqual(["Atari"]); + expect(out.publishers_logic).toBe("any"); + expect(out.developers).toEqual(["Artech Studios"]); + expect(out.developers_logic).toBe("all"); + }); + + it("omits the keys entirely when neither is selected", () => { + const out = buildSmartFilterCriteria(emptySnapshot()); + + expect(out.publishers).toBeUndefined(); + expect(out.developers).toBeUndefined(); + }); + + it("surfaces publishers and developers in the human-readable summary", () => { + const rows = summarizeSmartFilterCriteria( + { + publishers: ["Atari"], + publishers_logic: "any", + developers: ["Artech Studios"], + developers_logic: "any", + }, + tStub, + ); + + expect(rows.find((r) => r.key === "publishers")?.values).toEqual(["Atari"]); + expect(rows.find((r) => r.key === "developers")?.values).toEqual([ + "Artech Studios", + ]); + }); +}); + describe("buildSmartFilterCriteria — tags", () => { it("serializes selected tags with their logic operator", () => { const out = buildSmartFilterCriteria({ diff --git a/frontend/src/v2/utils/smartCollectionCriteria.ts b/frontend/src/v2/utils/smartCollectionCriteria.ts index d2b102485a..d302ed3b9d 100644 --- a/frontend/src/v2/utils/smartCollectionCriteria.ts +++ b/frontend/src/v2/utils/smartCollectionCriteria.ts @@ -43,6 +43,10 @@ export interface SmartFilterCriteria { collections_logic?: FilterLogic; companies?: string[]; companies_logic?: FilterLogic; + publishers?: string[]; + publishers_logic?: FilterLogic; + developers?: string[]; + developers_logic?: FilterLogic; age_ratings?: string[]; age_ratings_logic?: FilterLogic; regions?: string[]; @@ -81,6 +85,10 @@ export interface GalleryFilterSnapshot { collectionsLogic: FilterLogic; selectedCompanies: string[]; companiesLogic: FilterLogic; + selectedPublishers: string[]; + publishersLogic: FilterLogic; + selectedDevelopers: string[]; + developersLogic: FilterLogic; selectedAgeRatings: string[]; ageRatingsLogic: FilterLogic; selectedRegions: string[]; @@ -167,6 +175,14 @@ export function buildSmartFilterCriteria( out.companies = snap.selectedCompanies; out.companies_logic = snap.companiesLogic; } + if (snap.selectedPublishers.length > 0) { + out.publishers = snap.selectedPublishers; + out.publishers_logic = snap.publishersLogic; + } + if (snap.selectedDevelopers.length > 0) { + out.developers = snap.selectedDevelopers; + out.developers_logic = snap.developersLogic; + } if (snap.selectedAgeRatings.length > 0) { out.age_ratings = snap.selectedAgeRatings; out.age_ratings_logic = snap.ageRatingsLogic; @@ -364,6 +380,22 @@ const FIELDS: FieldSpec[] = [ defaultLabel: "Companies", kind: "list", }, + { + storage: "publishers", + logicStorage: "publishers_logic", + icon: "mdi-bank-outline", + labelKey: "platform.publisher", + defaultLabel: "Publishers", + kind: "list", + }, + { + storage: "developers", + logicStorage: "developers_logic", + icon: "mdi-code-tags", + labelKey: "platform.developer", + defaultLabel: "Developers", + kind: "list", + }, { storage: "age_ratings", logicStorage: "age_ratings_logic", diff --git a/frontend/src/v2/views/GameDetails.vue b/frontend/src/v2/views/GameDetails.vue index 19923274c8..1a750d5693 100644 --- a/frontend/src/v2/views/GameDetails.vue +++ b/frontend/src/v2/views/GameDetails.vue @@ -122,6 +122,12 @@ const franchises = computed( () => currentRom.value?.metadatum?.franchises ?? [], ); const companies = computed(() => currentRom.value?.metadatum?.companies ?? []); +const publishers = computed( + () => currentRom.value?.metadatum?.publishers ?? [], +); +const developers = computed( + () => currentRom.value?.metadatum?.developers ?? [], +); const collections = computed( () => currentRom.value?.metadatum?.collections ?? [], ); @@ -158,14 +164,21 @@ const lastPlayed = computed(() => { return new Date(ts).toLocaleString(); }); -// "Companies" (not "Developer") — the API field is a merged list of -// developers + publishers + other company roles produced by the backend -// (see flashpoint/gamelist/launchbox handlers); calling it Developer -// would be a lie. "Franchises" mirrors the singular→plural consistency -// of the surrounding rows. +// Prefer the explicit Developers / Publishers split; the merged "Companies" +// list is only shown as a fallback for ROMs scanned before the split was +// populated (InfoGrid drops empty sections). "Franchises" mirrors the +// singular→plural consistency of the surrounding rows. +const showMergedCompanies = computed( + () => publishers.value.length === 0 && developers.value.length === 0, +); const overviewSections = computed(() => [ { label: t("rom.genres"), items: genres.value }, - { label: t("rom.companies"), items: companies.value }, + { label: t("rom.developers"), items: developers.value }, + { label: t("rom.publishers"), items: publishers.value }, + { + label: t("rom.companies"), + items: showMergedCompanies.value ? companies.value : [], + }, { label: t("rom.franchises"), items: franchises.value }, { label: t("rom.collections"), items: collections.value }, ]); diff --git a/frontend/src/v2/views/Settings/UserInterface.vue b/frontend/src/v2/views/Settings/UserInterface.vue index ce6f1f3945..8aaa1f8c42 100644 --- a/frontend/src/v2/views/Settings/UserInterface.vue +++ b/frontend/src/v2/views/Settings/UserInterface.vue @@ -149,6 +149,8 @@ const virtualCollectionTypeItems = computed(() => [ { title: t("settings.vc-genre"), value: "genre" }, { title: t("settings.vc-mode"), value: "mode" }, { title: t("settings.vc-company"), value: "company" }, + { title: t("settings.vc-publisher"), value: "publisher" }, + { title: t("settings.vc-developer"), value: "developer" }, { title: t("settings.vc-all"), value: "all" }, ]);