diff --git a/backend/alembic/versions/0112_publisher_developer_split.py b/backend/alembic/versions/0112_publisher_developer_split.py new file mode 100644 index 0000000000..bc062fe7df --- /dev/null +++ b/backend/alembic/versions/0112_publisher_developer_split.py @@ -0,0 +1,417 @@ +"""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 threads the two new fields +through the read pipeline that 0098/0100/0101/0103 built. The +``virtual_collections`` view groups by ``(type, name)`` generically, so it +surfaces the new membership 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: 0112_publisher_developer_split +Revises: 0111_physical_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 = "0112_publisher_developer_split" +down_revision = "0111_physical_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"), +] + + +def _roms_metadata_view_sql(pg: bool, include_new: bool) -> str: + columns = _BASE_VIEW_COLUMNS + (_NEW_GENERATED 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 + 0103; 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"), +] +# Appended by 0103; the source column on `roms` carries the same name. +_FACETS_PROVIDER_IDS = [ + (name, name) + for name in ( + "igdb_id", + "ss_id", + "moby_id", + "launchbox_id", + "ra_id", + "hasheous_id", + "tgdb_id", + "flashpoint_id", + "hltb_id", + "gamelist_id", + "libretro_id", + ) +] +_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]]: + new = _FACETS_NEW if include_new else [] + return _FACETS_BASE + new + _FACETS_PROVIDER_IDS + + +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 _has_split_metadata(pg: bool) -> bool: + """Whether any row already carries the new keys. + + Only hand-written manual metadata can before a re-scan, so these backfills + would otherwise be a pair of full scans over a multi-gigabyte table for + nothing. + """ + empty = "'[]'::jsonb" if pg else "JSON_ARRAY()" + condition = ( + f"generated_publishers <> {empty} OR generated_developers <> {empty}" + if pg + else "JSON_LENGTH(generated_publishers) > 0" + " OR JSON_LENGTH(generated_developers) > 0" + ) + probe = sa.text(f"SELECT 1 FROM roms WHERE {condition} LIMIT 1") # nosec B608 + return op.get_bind().execute(probe).first() is not None + + +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)) + _rebuild_facets_triggers(pg, include_new=True) + _rebuild_vc_triggers(pg, _VC_ALL_TYPES) + + if _has_split_metadata(pg): + _backfill_facets_new_columns(pg) + # 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" + statement = ( + f"{insert} {TABLE} ({_VC_COLUMNS})\n{rows}\n{conflict}" # nosec B608 + ) + op.execute(statement) + + +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 9bec77c44f..63a5ba8a5c 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -108,6 +108,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, @@ -247,6 +249,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 @@ -269,6 +273,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) @@ -712,6 +724,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 0ef09a708d..b012dbe27a 100644 --- a/backend/endpoints/roms/__init__.py +++ b/backend/endpoints/roms/__init__.py @@ -503,6 +503,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( @@ -594,6 +612,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( @@ -698,6 +728,8 @@ def get_roms( franchises=franchises, collections=collections, companies=companies, + publishers=publishers, + developers=developers, age_ratings=age_ratings, statuses=statuses, regions=regions, @@ -710,6 +742,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, @@ -751,6 +785,8 @@ def get_roms( or franchises or collections or companies + or publishers + or developers or age_ratings or statuses or regions @@ -794,6 +830,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 ad7180e21d..21a395e76e 100644 --- a/backend/handler/database/collections_handler.py +++ b/backend/handler/database/collections_handler.py @@ -480,9 +480,12 @@ def get_smart_collection_criteria( """ criteria = smart_collection.filter_criteria - # Early versions stored single values under `selected_*` keys. - def as_list(new_key: str, old_key: str) -> list[str] | None: - value = criteria.get(new_key) or criteria.get(old_key) + # Early versions stored single values under `selected_*` keys, for the + # filters that already existed then. + def as_list(new_key: str, old_key: str | None = None) -> list[str] | None: + value = criteria.get(new_key) or ( + criteria.get(old_key) if old_key else None + ) if not value: return None return value if isinstance(value, list) else [value] @@ -511,6 +514,8 @@ def as_list(new_key: str, old_key: str) -> list[str] | None: "franchises": as_list("franchises", "selected_franchise"), "collections": as_list("collections", "selected_collection"), "companies": as_list("companies", "selected_company"), + "publishers": as_list("publishers"), + "developers": as_list("developers"), "age_ratings": as_list("age_ratings", "selected_age_rating"), "regions": as_list("regions", "selected_region"), "languages": as_list("languages", "selected_language"), @@ -522,6 +527,8 @@ def as_list(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 384976f91b..b26060ad07 100644 --- a/backend/handler/database/roms_handler.py +++ b/backend/handler/database/roms_handler.py @@ -181,6 +181,8 @@ RomFacets.franchises, RomFacets.collections, RomFacets.companies, + RomFacets.publishers, + RomFacets.developers, RomFacets.game_modes, RomFacets.age_ratings, RomFacets.player_count, @@ -1034,6 +1036,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, @@ -1194,6 +1222,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, @@ -1206,6 +1236,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", @@ -1352,6 +1384,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), @@ -1495,7 +1529,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: @@ -1624,6 +1667,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), @@ -1636,6 +1681,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"), @@ -2946,6 +2993,8 @@ def _collect_filter_values( franchises = set() collections = set() companies = set() + publishers = set() + developers = set() game_modes = set() age_ratings = set() player_counts = set() @@ -2955,7 +3004,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: @@ -2964,6 +3013,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: @@ -2983,6 +3036,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..419091e2ca 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 @@ -75,9 +77,14 @@ def extract_flashpoint_metadata(game: FlashpointGame) -> FlashpointMetadata: except (ValueError, TypeError): first_release_date = "" + publishers = pydash.compact([game["publisher"]]) + developers = pydash.compact([game["developer"]]) + return FlashpointMetadata( franchises=pydash.compact([game["series"]]), - companies=pydash.uniq(pydash.compact([game["developer"], game["publisher"]])), + companies=pydash.uniq([*developers, *publishers]), + publishers=publishers, + developers=developers, 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 c6918bf742..906b566319 100644 --- a/backend/handler/metadata/gamelist_handler.py +++ b/backend/handler/metadata/gamelist_handler.py @@ -61,6 +61,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 @@ -230,20 +232,16 @@ def extract_metadata_from_gamelist_rom( ) md5 = md5_elem.text if md5_elem is not None and md5_elem.text else None + publishers = _split_comma_separated_values(publisher) + developers = _split_comma_separated_values(developer) + return GamelistMetadata( rating=rating, first_release_date=first_release_date, sort_name=sort_name, - companies=list( - dict.fromkeys( - pydash.compact( - [ - *_split_comma_separated_values(developer), - *_split_comma_separated_values(publisher), - ] - ) - ) - ), + companies=list(dict.fromkeys([*developers, *publishers])), + publishers=publishers, + developers=developers, 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 9bd889976c..49cf7fd279 100644 --- a/backend/handler/metadata/hasheous_handler.py +++ b/backend/handler/metadata/hasheous_handler.py @@ -58,6 +58,18 @@ class HasheousRom(BaseRom): ACCEPTABLE_FILE_EXTENSIONS_BY_PLATFORM_SLUG = {UPS.DC: ["bin", "chd", "cue"]} +def _involved_company_names(rom: dict[str, Any], role: str) -> list[str]: + """Company names for an IGDB involvement role. + + The proxy keys its expanded lists by id, so involvements arrive as a dict + rather than the list IGDB itself returns. + """ + involved = pydash.values(rom.get("involved_companies", {})) + return pydash.compact( + pydash.map_([c for c in involved if c.get(role)], "company.name") + ) + + def extract_metadata_from_igdb_rom(rom: dict[str, Any]) -> IGDBMetadata: return IGDBMetadata( { @@ -88,6 +100,8 @@ def extract_metadata_from_igdb_rom(rom: dict[str, Any]) -> IGDBMetadata: "companies": pydash.compact( pydash.map_(rom.get("involved_companies", {}), "company.name") ), + "publishers": _involved_company_names(rom, "publisher"), + "developers": _involved_company_names(rom, "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 5d86db428f..75efa3713a 100644 --- a/backend/handler/metadata/launchbox_handler/types.py +++ b/backend/handler/metadata/launchbox_handler/types.py @@ -58,6 +58,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 da7b4de25a..c74f2617c0 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] @@ -97,12 +99,15 @@ def parse_release_timestamp(): except (AttributeError, ValueError, IndexError): return None + publishers = pydash.compact([rom_details.get("Publisher", None)]) + developers = pydash.compact([rom_details.get("Developer", None)]) + return RAMetadata( first_release_date=parse_release_timestamp(), genres=pydash.compact([rom_details.get("Genre", None)]), - companies=pydash.compact( - [rom_details.get("Publisher", None), rom_details.get("Developer", None)] - ), + companies=[*publishers, *developers], + publishers=publishers, + developers=developers, 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 567eaa703e..21a8a74794 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -330,6 +330,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] @@ -634,17 +636,17 @@ def _get_age_ratings(game: SSGame) -> list[SSAgeRating]: if classification.get("type") and classification.get("text") ] + publishers = pydash.compact([game.get("editeur", {}).get("text")]) + developers = pydash.compact([game.get("developpeur", {}).get("text")]) + return SSMetadata( { "ss_score": _normalize_score(game.get("note", {}).get("text", "")), "alternative_names": [name["text"] for name in game.get("noms", [])], "age_ratings": _get_age_ratings(game), - "companies": pydash.compact( - [ - game.get("editeur", {}).get("text"), - game.get("developpeur", {}).get("text"), - ] - ), + "companies": [*publishers, *developers], + "publishers": publishers, + "developers": developers, "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 5ae0e8df28..02d9233671 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -378,6 +378,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") @@ -386,6 +388,18 @@ class RomMetadata(BaseModel): rom: Mapped[Rom] = relationship(lazy="joined", back_populates="metadatum") + @property + def primary_developer(self) -> str | None: + """Developer for exporters, falling back to the pre-split companies ordering.""" + companies = self.companies or [] + return next(iter(self.developers or companies[:1]), None) + + @property + def primary_publisher(self) -> str | None: + """Publisher for exporters, falling back to the pre-split companies ordering.""" + companies = self.companies or [] + return next(iter(self.publishers or companies[1:2]), None) + class RomFacets(BaseModel): """Narrow mirror of the per-ROM values that back the filter dropdowns. @@ -410,6 +424,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/endpoints/roms/test_rom_sidecar_cache.py b/backend/tests/endpoints/roms/test_rom_sidecar_cache.py index 2cf7a70465..6f26383999 100644 --- a/backend/tests/endpoints/roms/test_rom_sidecar_cache.py +++ b/backend/tests/endpoints/roms/test_rom_sidecar_cache.py @@ -42,6 +42,8 @@ "franchises": [], "collections": [], "companies": [], + "publishers": [], + "developers": [], "game_modes": [], "age_ratings": [], "player_counts": [], diff --git a/backend/tests/handler/database/test_generated_metadata.py b/backend/tests/handler/database/test_generated_metadata.py index 71ef38ff13..743c9664ac 100644 --- a/backend/tests/handler/database/test_generated_metadata.py +++ b/backend/tests/handler/database/test_generated_metadata.py @@ -46,6 +46,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 454c87fae8..17d3bdfe0d 100644 --- a/backend/tests/handler/database/test_rom_facets.py +++ b/backend/tests/handler/database/test_rom_facets.py @@ -56,6 +56,27 @@ def test_provider_ids_mirror_the_rom(self, rom: Rom): # Sources the ROM didn't match stay null. assert facets.ss_id is None + 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/handler/metadata/test_hasheous_handler.py b/backend/tests/handler/metadata/test_hasheous_handler.py new file mode 100644 index 0000000000..048858abd7 --- /dev/null +++ b/backend/tests/handler/metadata/test_hasheous_handler.py @@ -0,0 +1,39 @@ +from handler.metadata.hasheous_handler import extract_metadata_from_igdb_rom + +# The proxy keys expanded collections by id and returns `company` as a bare id, +# unlike IGDB's own list-of-objects shape. +PROXY_ROM = { + "involved_companies": { + "148214": {"id": 148214, "company": 70, "developer": True, "publisher": False}, + "225579": {"id": 225579, "company": 812, "developer": False, "publisher": True}, + }, +} + +IGDB_ROM = { + "involved_companies": [ + {"company": {"name": "Retro Studios"}, "developer": True, "publisher": False}, + {"company": {"name": "Nintendo"}, "developer": False, "publisher": True}, + ], +} + + +def test_reads_the_proxys_dict_shaped_involvements(): + metadata = extract_metadata_from_igdb_rom(PROXY_ROM) + + assert metadata["companies"] == [] + assert metadata["publishers"] == [] + assert metadata["developers"] == [] + + +def test_reads_igdbs_list_shaped_involvements(): + metadata = extract_metadata_from_igdb_rom(IGDB_ROM) + + assert metadata["publishers"] == ["Nintendo"] + assert metadata["developers"] == ["Retro Studios"] + + +def test_involvements_are_optional(): + metadata = extract_metadata_from_igdb_rom({}) + + assert metadata["publishers"] == [] + assert metadata["developers"] == [] diff --git a/backend/tests/utils/test_gamelist_exporter.py b/backend/tests/utils/test_gamelist_exporter.py index 6596950c63..946393cfbc 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 ed53878933..44c2a25d35 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) @@ -431,6 +470,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 3f17b3c000..1da854fe3f 100644 --- a/backend/utils/gamelist_exporter.py +++ b/backend/utils/gamelist_exporter.py @@ -231,12 +231,12 @@ 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] + developer = rom.metadatum.primary_developer + publisher = rom.metadatum.primary_publisher + 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 04a7eb8ef4..e359c2d442 100644 --- a/backend/utils/pegasus_exporter.py +++ b/backend/utils/pegasus_exporter.py @@ -238,12 +238,13 @@ 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]}") + if rom.metadatum: + developer = rom.metadatum.primary_developer + publisher = rom.metadatum.primary_publisher + 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 dbb36b3262..a93c0924d1 100644 --- a/frontend/src/__generated__/models/RomGamelistMetadata.ts +++ b/frontend/src/__generated__/models/RomGamelistMetadata.ts @@ -21,6 +21,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 4e683ddcb3..5f14ce113c 100644 --- a/frontend/src/__generated__/models/RomSSMetadata.ts +++ b/frontend/src/__generated__/models/RomSSMetadata.ts @@ -41,6 +41,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 97fe8bf8b5..231a392000 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": "Разработчик", "download-platform": "Изтегли платформа", "edit-platform": "Редактирай платформата", "export": "Експортирай", @@ -53,6 +54,7 @@ "only-with-games": "Само платформи с игри", "player-count": "Брой играчи", "properties": "Свойства", + "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 47b27ef8eb..32bf5d723e 100644 --- a/frontend/src/locales/bg_BG/rom.json +++ b/frontend/src/locales/bg_BG/rom.json @@ -123,6 +123,7 @@ "deleted-from-database": "{count} ROM изтрит от базата данни | {count} ROM-а изтрити от базата данни", "deleted-from-filesystem": "{count} ROM изтрит от файловата система | {count} ROM-а изтрити от файловата система", "details": "Детайли", + "developers": "Разработчици", "difficulty": "Трудност", "disc": "Disc", "download": "Свали", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Публично", "public-notes": "Публични бележки", + "publishers": "Издатели", "qr-scan-to-download": "Сканирай, за да свалиш", "rating": "Оценка", "ratings-label": "Оценки", diff --git a/frontend/src/locales/bg_BG/settings.json b/frontend/src/locales/bg_BG/settings.json index 6acc19166c..1f11973dcc 100644 --- a/frontend/src/locales/bg_BG/settings.json +++ b/frontend/src/locales/bg_BG/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Папка с варианти", "vc-all": "Всички (бавно)", "vc-collection": "Колекция", - "vc-company": "Разработчик", + "vc-company": "Компания", + "vc-developer": "Разработчик", "vc-franchise": "Франчайз", "vc-genre": "Жанр", "vc-mode": "Режим на игра", + "vc-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 1e4c0ced5d..37c40262a7 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": "Vývojář", "download-platform": "Stáhnout platformu", "edit-platform": "Upravit platformu", "export": "Export", @@ -53,6 +54,7 @@ "only-with-games": "Pouze platformy s hrami", "player-count": "Počet hráčů", "properties": "Vlastnosti", + "publisher": "Vydavatel", "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 52e6b4b810..c1fdd7a7ed 100644 --- a/frontend/src/locales/cs_CZ/rom.json +++ b/frontend/src/locales/cs_CZ/rom.json @@ -123,6 +123,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": "Vývojáři", "difficulty": "Obtížnost", "disc": "Disc", "download": "Stáhnout", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Veřejné", "public-notes": "Veřejné poznámky", + "publishers": "Vydavatelé", "qr-scan-to-download": "Naskenujte pro stažení", "rating": "Hodnocení", "ratings-label": "Hodnocení", diff --git a/frontend/src/locales/cs_CZ/settings.json b/frontend/src/locales/cs_CZ/settings.json index 57b3c45720..25ffd1c8b3 100644 --- a/frontend/src/locales/cs_CZ/settings.json +++ b/frontend/src/locales/cs_CZ/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Varianta složky", "vc-all": "Vše (pomalé)", "vc-collection": "Kolekce", - "vc-company": "Vývojář", + "vc-company": "Společnost", + "vc-developer": "Vývojář", "vc-franchise": "Franšíza", "vc-genre": "Žánr", "vc-mode": "Herní režim", + "vc-publisher": "Vydavatel", "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 9c818bf9df..7e59d9783f 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": "Entwickler", "download-platform": "Plattform herunterladen", "edit-platform": "Plattform bearbeiten", "export": "Exportieren", @@ -53,6 +54,7 @@ "only-with-games": "Nur Plattformen mit Spielen", "player-count": "Spieleranzahl", "properties": "Eigenschaften", + "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 4e6c1e176c..46932d3905 100644 --- a/frontend/src/locales/de_DE/rom.json +++ b/frontend/src/locales/de_DE/rom.json @@ -123,6 +123,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": "Entwickler", "difficulty": "Schwierigkeitsgrad", "disc": "Disc", "download": "Herunterladen", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB-ID", "public": "Öffentlich", "public-notes": "Öffentliche Notizen", + "publishers": "Publisher", "qr-scan-to-download": "Scannen zum Herunterladen", "rating": "Bewertung", "ratings-label": "Bewertungen", diff --git a/frontend/src/locales/de_DE/settings.json b/frontend/src/locales/de_DE/settings.json index f7edf1d905..7f468bbc8d 100644 --- a/frontend/src/locales/de_DE/settings.json +++ b/frontend/src/locales/de_DE/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Variantenordner", "vc-all": "Alle (langsam)", "vc-collection": "Sammlung", - "vc-company": "Entwickler", + "vc-company": "Unternehmen", + "vc-developer": "Entwickler", "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 daaa34a08b..2b250cbad3 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", @@ -53,6 +54,7 @@ "only-with-games": "Only platforms with games", "player-count": "Player count", "properties": "Properties", + "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 d13b1b57f0..97748029a6 100644 --- a/frontend/src/locales/en_GB/rom.json +++ b/frontend/src/locales/en_GB/rom.json @@ -123,6 +123,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", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Public", "public-notes": "Public notes", + "publishers": "Publishers", "qr-scan-to-download": "Scan to download", "rating": "Rating", "ratings-label": "Ratings", diff --git a/frontend/src/locales/en_GB/settings.json b/frontend/src/locales/en_GB/settings.json index a326dfff98..c1dfca7f43 100644 --- a/frontend/src/locales/en_GB/settings.json +++ b/frontend/src/locales/en_GB/settings.json @@ -489,10 +489,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 86f31991de..d5e1372def 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", @@ -53,6 +54,7 @@ "only-with-games": "Only platforms with games", "player-count": "Player count", "properties": "Properties", + "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 20643920c3..7cf5883079 100644 --- a/frontend/src/locales/en_US/rom.json +++ b/frontend/src/locales/en_US/rom.json @@ -123,6 +123,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", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Public", "public-notes": "Public notes", + "publishers": "Publishers", "qr-scan-to-download": "Scan to download", "rating": "Rating", "ratings-label": "Ratings", diff --git a/frontend/src/locales/en_US/settings.json b/frontend/src/locales/en_US/settings.json index 5022009e8c..4b200cef04 100644 --- a/frontend/src/locales/en_US/settings.json +++ b/frontend/src/locales/en_US/settings.json @@ -489,10 +489,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 0fe982d8c7..609bf5e19f 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": "Desarrollador", "download-platform": "Descargar plataforma", "edit-platform": "Editar plataforma", "export": "Exportar", @@ -53,6 +54,7 @@ "only-with-games": "Solo plataformas con juegos", "player-count": "Número de jugadores", "properties": "Propiedades", + "publisher": "Distribuidora", "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 93c40baf34..3c81eb24e8 100644 --- a/frontend/src/locales/es_ES/rom.json +++ b/frontend/src/locales/es_ES/rom.json @@ -123,6 +123,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": "Desarrolladores", "difficulty": "Dificultad", "disc": "Disc", "download": "Descargar", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "ID de SteamGridDB", "public": "Pública", "public-notes": "Notas públicas", + "publishers": "Distribuidoras", "qr-scan-to-download": "Escanear para descargar", "rating": "Valoración", "ratings-label": "Valoraciones", diff --git a/frontend/src/locales/es_ES/settings.json b/frontend/src/locales/es_ES/settings.json index 7fa8e1296e..3470da684d 100644 --- a/frontend/src/locales/es_ES/settings.json +++ b/frontend/src/locales/es_ES/settings.json @@ -490,9 +490,11 @@ "vc-all": "Todo", "vc-collection": "Colección", "vc-company": "Compañía", + "vc-developer": "Desarrollador", "vc-franchise": "Franquicia", "vc-genre": "Género", "vc-mode": "Modo de juego", + "vc-publisher": "Distribuidora", "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 665d27091c..086c74c994 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": "Développeur", "download-platform": "Télécharger la plateforme", "edit-platform": "Modifier la plateforme", "export": "Exporter", @@ -53,6 +54,7 @@ "only-with-games": "Uniquement les plateformes avec des jeux", "player-count": "Nombre de joueurs", "properties": "Propriétés", + "publisher": "Éditeur", "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 2a15fa6d13..dc2b9445b9 100644 --- a/frontend/src/locales/fr_FR/rom.json +++ b/frontend/src/locales/fr_FR/rom.json @@ -123,6 +123,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": "Développeurs", "difficulty": "Difficulté", "disc": "Disc", "download": "Télécharger", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "ID SteamGridDB", "public": "Publique", "public-notes": "Notes publiques", + "publishers": "Éditeurs", "qr-scan-to-download": "Scanner pour télécharger", "rating": "Note", "ratings-label": "Notes", diff --git a/frontend/src/locales/fr_FR/settings.json b/frontend/src/locales/fr_FR/settings.json index dd68472cab..a9c94b5d90 100644 --- a/frontend/src/locales/fr_FR/settings.json +++ b/frontend/src/locales/fr_FR/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Dossier variante", "vc-all": "Tout (lent)", "vc-collection": "Collection", - "vc-company": "Développeur", + "vc-company": "Entreprise", + "vc-developer": "Développeur", "vc-franchise": "Franchise", "vc-genre": "Genre", "vc-mode": "Mode de jeu", + "vc-publisher": "Éditeur", "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 9c9b554822..8ab62cb3dd 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": "Fejlesztő", "download-platform": "Platform letöltése", "edit-platform": "Platform szerkesztése", "export": "Exportálás", @@ -53,6 +54,7 @@ "only-with-games": "Csak játékokkal rendelkező platformok", "player-count": "Játékosok száma", "properties": "Tulajdonságok", + "publisher": "Kiadó", "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 02bd0abe35..4790d8c601 100644 --- a/frontend/src/locales/hu_HU/rom.json +++ b/frontend/src/locales/hu_HU/rom.json @@ -123,6 +123,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": "Fejlesztők", "difficulty": "Nehézség", "disc": "Disc", "download": "Letöltés", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB azonosító", "public": "Publikus", "public-notes": "Publikus jegyzetek", + "publishers": "Kiadók", "qr-scan-to-download": "Olvasd be a letöltéshez", "rating": "Értékelés", "ratings-label": "Értékelések", diff --git a/frontend/src/locales/hu_HU/settings.json b/frontend/src/locales/hu_HU/settings.json index 0e4a4f7b14..6c3f3569c3 100644 --- a/frontend/src/locales/hu_HU/settings.json +++ b/frontend/src/locales/hu_HU/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Változat mappa", "vc-all": "Összes (lassú)", "vc-collection": " yűjtemény", - "vc-company": "Fejlesztő", + "vc-company": "Cég", + "vc-developer": "Fejlesztő", "vc-franchise": "Franchise", "vc-genre": "Műfaj", "vc-mode": "Játékmód", + "vc-publisher": "Kiadó", "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 ae100fe8cc..fc7f1639a7 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": "Sviluppatore", "download-platform": "Scarica piattaforma", "edit-platform": "Modifica piattaforma", "export": "Esporta", @@ -53,6 +54,7 @@ "only-with-games": "Solo piattaforme con giochi", "player-count": "Numero di giocatori", "properties": "Proprietà", + "publisher": "Editore", "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 a5cbdf1f1c..ef0be0ec40 100644 --- a/frontend/src/locales/it_IT/rom.json +++ b/frontend/src/locales/it_IT/rom.json @@ -123,6 +123,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": "Sviluppatori", "difficulty": "Difficoltà", "disc": "Disc", "download": "Scarica", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "ID SteamGridDB", "public": "Pubblica", "public-notes": "Note pubbliche", + "publishers": "Editori", "qr-scan-to-download": "Scansiona per scaricare", "rating": "Valutazione", "ratings-label": "Valutazioni", diff --git a/frontend/src/locales/it_IT/settings.json b/frontend/src/locales/it_IT/settings.json index f8503d5b3c..6777e696b7 100644 --- a/frontend/src/locales/it_IT/settings.json +++ b/frontend/src/locales/it_IT/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Cartella variante", "vc-all": "Tutto (lento)", "vc-collection": "Collezione", - "vc-company": "Sviluppatore", + "vc-company": "Compagnia", + "vc-developer": "Sviluppatore", "vc-franchise": "Franchise", "vc-genre": "Genere", "vc-mode": "Modalità di gioco", + "vc-publisher": "Editore", "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 c2a77f094c..e81477de60 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": "開発元", "download-platform": "プラットフォームをダウンロード", "edit-platform": "プラットフォームを編集", "export": "エクスポート", @@ -53,6 +54,7 @@ "only-with-games": "ゲームがあるプラットフォームのみ", "player-count": "プレイヤー数", "properties": "属性", + "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 2c27eaf558..2745d060cd 100644 --- a/frontend/src/locales/ja_JP/rom.json +++ b/frontend/src/locales/ja_JP/rom.json @@ -123,6 +123,7 @@ "deleted-from-database": "{count}件のROMがデータベースから削除されました", "deleted-from-filesystem": "{count}件のROMがファイルシステムから削除されました", "details": "詳細", + "developers": "開発元", "difficulty": "難易度", "disc": "Disc", "download": "ダウンロード", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "公開", "public-notes": "公開メモ", + "publishers": "発売元", "qr-scan-to-download": "スキャンしてダウンロード", "rating": "レーティング", "ratings-label": "レーティング", diff --git a/frontend/src/locales/ja_JP/settings.json b/frontend/src/locales/ja_JP/settings.json index 27cbf1ecb1..a61a7fe915 100644 --- a/frontend/src/locales/ja_JP/settings.json +++ b/frontend/src/locales/ja_JP/settings.json @@ -489,10 +489,12 @@ "variant-folder": "バリアントフォルダ", "vc-all": "すべて(低速)", "vc-collection": "コレクション", - "vc-company": "デベロッパー", + "vc-company": "会社", + "vc-developer": "開発元", "vc-franchise": "フランチャイズ", "vc-genre": "ジャンル", "vc-mode": "プレイモード", + "vc-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 f27380dc9b..f0a4cf5965 100644 --- a/frontend/src/locales/ko_KR/platform.json +++ b/frontend/src/locales/ko_KR/platform.json @@ -7,12 +7,13 @@ "change-view": "배열 방식 변경", "cloud-source": "클라우드", "collection": "모음집", - "company": "개발사", + "company": "회사", "cover-style": "커버 스타일", "danger-zone": "위험 구역", "delete-platform": "플랫폼 지우기", "delete-platform-hint": "플랫폼과 해당 ROM 데이터베이스 항목을 제거합니다. 디스크의 파일은 삭제되지 않습니다.", "description": "설명", + "developer": "개발사", "download-platform": "플랫폼 다운로드", "edit-platform": "플랫폼 편집", "export": "내보내기", @@ -53,6 +54,7 @@ "only-with-games": "게임이 있는 플랫폼만", "player-count": "플레이어 수", "properties": "속성", + "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 0de5d96cfc..9c2e956569 100644 --- a/frontend/src/locales/ko_KR/rom.json +++ b/frontend/src/locales/ko_KR/rom.json @@ -64,7 +64,7 @@ "collection-remove-failed": "모음집에서 제거할 수 없습니다", "collections": "시리즈", "community-notes": "커뮤니티 노트", - "companies": "개발사", + "companies": "회사", "completion": "완료율", "completionist": "완주주의자", "confirm-delete-note": "노트 \"{title}\"을(를) 삭제하시겠습니까?", @@ -123,6 +123,7 @@ "deleted-from-database": "{count}개의 ROM이 데이터베이스에서 삭제되었습니다", "deleted-from-filesystem": "{count}개의 ROM이 파일 시스템에서 삭제되었습니다", "details": "자세히", + "developers": "개발사", "difficulty": "난이도", "disc": "Disc", "download": "다운로드", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "공개", "public-notes": "공개 노트", + "publishers": "배급사", "qr-scan-to-download": "스캔하여 다운로드", "rating": "평가", "ratings-label": "평가", diff --git a/frontend/src/locales/ko_KR/settings.json b/frontend/src/locales/ko_KR/settings.json index 0abe95d2e3..5e3e5a97a0 100644 --- a/frontend/src/locales/ko_KR/settings.json +++ b/frontend/src/locales/ko_KR/settings.json @@ -489,10 +489,12 @@ "variant-folder": "변형 폴더", "vc-all": "전체 (느림)", "vc-collection": "컬렉션", - "vc-company": "개발사", + "vc-company": "회사", + "vc-developer": "개발사", "vc-franchise": "프랜차이즈", "vc-genre": "장르", "vc-mode": "플레이 모드", + "vc-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 a59fdfbce0..d359ed0495 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": "Deweloper", "download-platform": "Pobierz platforme", "edit-platform": "Edytuj platformę", "export": "Eksportuj", @@ -53,6 +54,7 @@ "only-with-games": "Tylko platformy z grami", "player-count": "Liczba graczy", "properties": "Właściwości", + "publisher": "Wydawca", "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 44705e0891..d5af89b819 100644 --- a/frontend/src/locales/pl_PL/rom.json +++ b/frontend/src/locales/pl_PL/rom.json @@ -123,6 +123,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": "Deweloperzy", "difficulty": "Poziom trudności", "disc": "Disc", "download": "Pobierz", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Publiczna", "public-notes": "Notatki publiczne", + "publishers": "Wydawcy", "qr-scan-to-download": "Zeskanuj, aby pobrać", "rating": "Ocena", "ratings-label": "Oceny", diff --git a/frontend/src/locales/pl_PL/settings.json b/frontend/src/locales/pl_PL/settings.json index 95f4672716..b6ae5de825 100644 --- a/frontend/src/locales/pl_PL/settings.json +++ b/frontend/src/locales/pl_PL/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Folder wariantu", "vc-all": "Wszystkie (powoli)", "vc-collection": "Kolekcja", - "vc-company": "Deweloper", + "vc-company": "Firma", + "vc-developer": "Deweloper", "vc-franchise": "Seria", "vc-genre": "Gatunek", "vc-mode": "Tryb gry", + "vc-publisher": "Wydawca", "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 ef8ceec0cd..cb29113a87 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": "Desenvolvedor", "download-platform": "Baixar plataforma", "edit-platform": "Editar plataforma", "export": "Exportar", @@ -53,6 +54,7 @@ "only-with-games": "Apenas plataformas com jogos", "player-count": "Número de jogadores", "properties": "Propriedades", + "publisher": "Distribuidora", "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 04efcbf00c..69c79c8fc7 100644 --- a/frontend/src/locales/pt_BR/rom.json +++ b/frontend/src/locales/pt_BR/rom.json @@ -123,6 +123,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": "Desenvolvedores", "difficulty": "Dificuldade", "disc": "Disco", "download": "Baixar", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "ID do SteamGridDB", "public": "Pública", "public-notes": "Notas públicas", + "publishers": "Distribuidoras", "qr-scan-to-download": "Escaneie para baixar", "rating": "Avaliação", "ratings-label": "Avaliações", diff --git a/frontend/src/locales/pt_BR/settings.json b/frontend/src/locales/pt_BR/settings.json index ab2b6ee7d3..6283a411f6 100644 --- a/frontend/src/locales/pt_BR/settings.json +++ b/frontend/src/locales/pt_BR/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Pasta variante", "vc-all": "Todos (lento)", "vc-collection": "Coleção", - "vc-company": "Desenvolvedora", + "vc-company": "Empresa", + "vc-developer": "Desenvolvedor", "vc-franchise": "Franquia", "vc-genre": "Gênero", "vc-mode": "Modo de jogo", + "vc-publisher": "Distribuidora", "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 2edac15c53..e64db9f7f3 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": "Dezvoltator", "download-platform": "Descarcă platforma", "edit-platform": "Editează platforma", "export": "Exportare", @@ -53,6 +54,7 @@ "only-with-games": "Doar platformele cu jocuri", "player-count": "Numărul de jucători", "properties": "Proprietăți", + "publisher": "Editor", "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 fe1194ad59..dee9095855 100644 --- a/frontend/src/locales/ro_RO/rom.json +++ b/frontend/src/locales/ro_RO/rom.json @@ -123,6 +123,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": "Dezvoltatori", "difficulty": "Dificultate", "disc": "Disc", "download": "Descarcă", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "ID SteamGridDB", "public": "Publică", "public-notes": "Notițe publice", + "publishers": "Editori", "qr-scan-to-download": "Scanează pentru a descărca", "rating": "Evaluare", "ratings-label": "Evaluări", diff --git a/frontend/src/locales/ro_RO/settings.json b/frontend/src/locales/ro_RO/settings.json index e7d8d6c037..be1a8607b6 100644 --- a/frontend/src/locales/ro_RO/settings.json +++ b/frontend/src/locales/ro_RO/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Dosar variantă", "vc-all": "Toate (lent)", "vc-collection": "Colecție", - "vc-company": "Dezvoltator", + "vc-company": "Companie", + "vc-developer": "Dezvoltator", "vc-franchise": "Franciză", "vc-genre": "Gen", "vc-mode": "Mod de joc", + "vc-publisher": "Editor", "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 7cfbc9e2f1..0a3535ca92 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": "Разработчик", "download-platform": "Скачать платформу", "edit-platform": "Редактировать платформу", "export": "Экспорт", @@ -53,6 +54,7 @@ "only-with-games": "Только платформы с играми", "player-count": "Количество игроков", "properties": "Свойства", + "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 bb54d6ae5d..70bd9d08b8 100644 --- a/frontend/src/locales/ru_RU/rom.json +++ b/frontend/src/locales/ru_RU/rom.json @@ -123,6 +123,7 @@ "deleted-from-database": "{count} ROM удалён из базы данных | {count} ROM-ов удалено из базы данных", "deleted-from-filesystem": "{count} ROM удалён из файловой системы | {count} ROM-ов удалено из файловой системы", "details": "Детали", + "developers": "Разработчики", "difficulty": "Сложность", "disc": "Disc", "download": "Скачать", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Публичная", "public-notes": "Публичные заметки", + "publishers": "Издатели", "qr-scan-to-download": "Отсканируйте для скачивания", "rating": "Рейтинг", "ratings-label": "Оценки", diff --git a/frontend/src/locales/ru_RU/settings.json b/frontend/src/locales/ru_RU/settings.json index a06e523114..55e5677f9c 100644 --- a/frontend/src/locales/ru_RU/settings.json +++ b/frontend/src/locales/ru_RU/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Папка варианта", "vc-all": "Все (медленно)", "vc-collection": "Коллекция", - "vc-company": "Разработчик", + "vc-company": "Компания", + "vc-developer": "Разработчик", "vc-franchise": "Франшиза", "vc-genre": "Жанр", "vc-mode": "Режим игры", + "vc-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 3d87acef59..b2a363400e 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": "Geliştirici", "download-platform": "Platformu indir", "edit-platform": "Platformu düzenle", "export": "Dışa aktar", @@ -53,6 +54,7 @@ "only-with-games": "Yalnızca oyunu olan platformlar", "player-count": "Oyuncu sayısı", "properties": "Özellikler", + "publisher": "Yayıncı", "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 688ed07efe..182dd950c0 100644 --- a/frontend/src/locales/tr_TR/rom.json +++ b/frontend/src/locales/tr_TR/rom.json @@ -123,6 +123,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": "Geliştiriciler", "difficulty": "Zorluk", "disc": "Disk", "download": "İndir", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "Herkese açık", "public-notes": "Herkese açık notlar", + "publishers": "Yayıncılar", "qr-scan-to-download": "İndirmek için tarat", "rating": "Puan", "ratings-label": "Puanlar", diff --git a/frontend/src/locales/tr_TR/settings.json b/frontend/src/locales/tr_TR/settings.json index 498a739c56..4da2f39486 100644 --- a/frontend/src/locales/tr_TR/settings.json +++ b/frontend/src/locales/tr_TR/settings.json @@ -489,10 +489,12 @@ "variant-folder": "Varyant klasörü", "vc-all": "Tümü (yavaş)", "vc-collection": "Koleksiyonu", - "vc-company": "Geliştirici", + "vc-company": "Şirket", + "vc-developer": "Geliştirici", "vc-franchise": "Franchise", "vc-genre": "Tür", "vc-mode": "Oyun Modu", + "vc-publisher": "Yayıncı", "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 c52af7cc85..d6a8bca80f 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": "开发商", "download-platform": "下载平台", "edit-platform": "编辑平台", "export": "导出", @@ -53,6 +54,7 @@ "only-with-games": "仅显示有游戏的平台", "player-count": "玩家人数", "properties": "属性", + "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 7ee25afca8..43355a8861 100644 --- a/frontend/src/locales/zh_CN/rom.json +++ b/frontend/src/locales/zh_CN/rom.json @@ -123,6 +123,7 @@ "deleted-from-database": "{count} 个 ROM 已从数据库删除", "deleted-from-filesystem": "{count} 个 ROM 已从文件系统删除", "details": "详情", + "developers": "开发商", "difficulty": "难度", "disc": "Disc", "download": "下载", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "公开", "public-notes": "公共笔记", + "publishers": "发行商", "qr-scan-to-download": "扫码下载", "rating": "评分", "ratings-label": "评分", diff --git a/frontend/src/locales/zh_CN/settings.json b/frontend/src/locales/zh_CN/settings.json index 625182111b..7a7c7cba3c 100644 --- a/frontend/src/locales/zh_CN/settings.json +++ b/frontend/src/locales/zh_CN/settings.json @@ -489,10 +489,12 @@ "variant-folder": "变体文件夹", "vc-all": "全部(较慢)", "vc-collection": "收藏", - "vc-company": "开发商", + "vc-company": "公司", + "vc-developer": "开发商", "vc-franchise": "系列", "vc-genre": "类型", "vc-mode": "游玩模式", + "vc-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 414fde82ba..abc7018b55 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": "開發商", "download-platform": "下載平台", "edit-platform": "編輯平台", "export": "導出", @@ -53,6 +54,7 @@ "only-with-games": "僅顯示有遊戲的平台", "player-count": "玩家人數", "properties": "屬性", + "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 38c05a21f0..7699838d8f 100644 --- a/frontend/src/locales/zh_TW/rom.json +++ b/frontend/src/locales/zh_TW/rom.json @@ -123,6 +123,7 @@ "deleted-from-database": "{count} 個 ROM 已從資料庫刪除", "deleted-from-filesystem": "{count} 個 ROM 已從檔案系統刪除", "details": "詳情", + "developers": "開發商", "difficulty": "難度", "disc": "Disc", "download": "下載", @@ -308,6 +309,7 @@ "provider-steamgriddb-id": "SteamGridDB ID", "public": "公開", "public-notes": "公共筆記", + "publishers": "發行商", "qr-scan-to-download": "掃描以下載", "rating": "評分", "ratings-label": "評分", diff --git a/frontend/src/locales/zh_TW/settings.json b/frontend/src/locales/zh_TW/settings.json index 2aa551a3ad..ebc7242724 100644 --- a/frontend/src/locales/zh_TW/settings.json +++ b/frontend/src/locales/zh_TW/settings.json @@ -489,10 +489,12 @@ "variant-folder": "變體資料夾", "vc-all": "全部(緩慢)", "vc-collection": "收藏", - "vc-company": "開發商", + "vc-company": "公司", + "vc-developer": "開發商", "vc-franchise": "系列", "vc-genre": "類型", "vc-mode": "遊玩模式", + "vc-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 50e365d803..5c5cff6f41 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -167,6 +167,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; @@ -179,6 +181,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; @@ -220,6 +224,8 @@ async function getRoms({ selectedFranchises = null, selectedCollections = null, selectedCompanies = null, + selectedPublishers = null, + selectedDevelopers = null, selectedAgeRatings = null, selectedRegions = null, selectedLanguages = null, @@ -232,6 +238,8 @@ async function getRoms({ franchisesLogic = null, collectionsLogic = null, companiesLogic = null, + publishersLogic = null, + developersLogic = null, ageRatingsLogic = null, regionsLogic = null, languagesLogic = null, @@ -271,6 +279,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 @@ -313,6 +329,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 8d55543676..938ca9880d 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[], @@ -53,6 +57,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[], @@ -65,6 +71,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, @@ -96,6 +104,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; }, @@ -145,6 +159,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; }, @@ -434,6 +460,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 || @@ -453,6 +481,8 @@ export default defineStore("galleryFilter", { this.selectedFranchises = []; this.selectedCollections = []; this.selectedCompanies = []; + this.selectedPublishers = []; + this.selectedDevelopers = []; this.selectedAgeRatings = []; this.selectedRegions = []; this.selectedLanguages = []; @@ -476,6 +506,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 d8b0c07902..a2816e17b0 100644 --- a/frontend/src/v2/components/Dialogs/CreateSmartCollectionDialog.vue +++ b/frontend/src/v2/components/Dialogs/CreateSmartCollectionDialog.vue @@ -104,6 +104,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 af6ddb7bef..3b8a89a495 100644 --- a/frontend/src/v2/components/Gallery/GalleryShell.vue +++ b/frontend/src/v2/components/Gallery/GalleryShell.vue @@ -170,6 +170,8 @@ const { selectedFranchises, selectedCollections, selectedCompanies, + selectedPublishers, + selectedDevelopers, selectedAgeRatings, selectedRegions, selectedLanguages, @@ -181,6 +183,8 @@ const { franchisesLogic, collectionsLogic, companiesLogic, + publishersLogic, + developersLogic, ageRatingsLogic, regionsLogic, languagesLogic, @@ -216,6 +220,8 @@ const filterActiveCount = computed(() => { selectedFranchises, selectedCollections, selectedCompanies, + selectedPublishers, + selectedDevelopers, selectedAgeRatings, selectedRegions, selectedLanguages, @@ -252,6 +258,8 @@ watch( selectedFranchises, selectedCollections, selectedCompanies, + selectedPublishers, + selectedDevelopers, selectedAgeRatings, selectedRegions, selectedLanguages, @@ -263,6 +271,8 @@ watch( franchisesLogic, collectionsLogic, companiesLogic, + publishersLogic, + developersLogic, ageRatingsLogic, regionsLogic, languagesLogic, diff --git a/frontend/src/v2/components/GameDetails/OverviewTab.test.ts b/frontend/src/v2/components/GameDetails/OverviewTab.test.ts index d4b465ece9..4db8677bbb 100644 --- a/frontend/src/v2/components/GameDetails/OverviewTab.test.ts +++ b/frontend/src/v2/components/GameDetails/OverviewTab.test.ts @@ -15,6 +15,8 @@ const EMPTY_METADATA: RomMetadataSchema = { franchises: [], collections: [], companies: [], + publishers: [], + developers: [], game_modes: [], age_ratings: [], player_count: "", diff --git a/frontend/src/v2/components/GameDetails/OverviewTab.vue b/frontend/src/v2/components/GameDetails/OverviewTab.vue index 0b3984d178..94f851d3f3 100644 --- a/frontend/src/v2/components/GameDetails/OverviewTab.vue +++ b/frontend/src/v2/components/GameDetails/OverviewTab.vue @@ -7,8 +7,8 @@ // 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 / Companies / +// Franchises / Collections) // 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 b15059bfdf..556ff8fe9a 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 fd5293c7aa..8d75b370aa 100644 --- a/frontend/src/v2/composables/useGalleryFilterUrl/index.ts +++ b/frontend/src/v2/composables/useGalleryFilterUrl/index.ts @@ -26,6 +26,8 @@ // ?franchises=…&franchisesLogic=… // ?collections=…&collectionsLogic=… // ?companies=…&companiesLogic=… +// ?publishers=…&publishersLogic=… +// ?developers=…&developersLogic=… // ?ageRatings=…&ageRatingsLogic=… // ?regions=…®ionsLogic=… // ?languages=…&languagesLogic=… @@ -116,6 +118,8 @@ export function useGalleryFilterUrl() { selectedFranchises, selectedCollections, selectedCompanies, + selectedPublishers, + selectedDevelopers, selectedAgeRatings, selectedRegions, selectedLanguages, @@ -127,6 +131,8 @@ export function useGalleryFilterUrl() { franchisesLogic, collectionsLogic, companiesLogic, + publishersLogic, + developersLogic, ageRatingsLogic, regionsLogic, languagesLogic, @@ -167,6 +173,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), @@ -245,6 +255,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) @@ -366,6 +386,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", @@ -447,6 +477,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 965c9bcf0a..cf06806882 100644 --- a/frontend/src/v2/stores/galleryRoms.ts +++ b/frontend/src/v2/stores/galleryRoms.ts @@ -363,6 +363,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, @@ -374,6 +376,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, @@ -418,6 +422,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 b6246c34ad..837c8fa4af 100644 --- a/frontend/src/v2/utils/smartCollectionCriteria.test.ts +++ b/frontend/src/v2/utils/smartCollectionCriteria.test.ts @@ -35,6 +35,10 @@ function emptySnapshot(): GalleryFilterSnapshot { collectionsLogic: "any", selectedCompanies: [], companiesLogic: "any", + selectedPublishers: [], + publishersLogic: "any", + selectedDevelopers: [], + developersLogic: "any", selectedAgeRatings: [], ageRatingsLogic: "any", selectedRegions: [], @@ -84,6 +88,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 22304a3648..f57e4d9989 100644 --- a/frontend/src/v2/utils/smartCollectionCriteria.ts +++ b/frontend/src/v2/utils/smartCollectionCriteria.ts @@ -46,6 +46,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[]; @@ -87,6 +91,10 @@ export interface GalleryFilterSnapshot { collectionsLogic: FilterLogic; selectedCompanies: string[]; companiesLogic: FilterLogic; + selectedPublishers: string[]; + publishersLogic: FilterLogic; + selectedDevelopers: string[]; + developersLogic: FilterLogic; selectedAgeRatings: string[]; ageRatingsLogic: FilterLogic; selectedRegions: string[]; @@ -179,6 +187,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; @@ -422,6 +438,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 8d33413fb9..250cfec7a5 100644 --- a/frontend/src/v2/views/GameDetails.vue +++ b/frontend/src/v2/views/GameDetails.vue @@ -125,6 +125,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 ?? [], ); @@ -161,14 +167,28 @@ 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. +// Companies is a merged developers + publishers list, so show it only when +// both split fields are empty (InfoGrid drops empty sections). +const showMergedCompanies = computed( + () => publishers.value.length === 0 && developers.value.length === 0, +); const overviewSections = computed(() => [ { label: t("rom.genres"), items: genres.value, filter: "genres" }, - { label: t("rom.companies"), items: companies.value, filter: "companies" }, + { + label: t("rom.developers"), + items: developers.value, + filter: "developers", + }, + { + label: t("rom.publishers"), + items: publishers.value, + filter: "publishers", + }, + { + label: t("rom.companies"), + items: showMergedCompanies.value ? companies.value : [], + filter: "companies", + }, { label: t("rom.franchises"), items: franchises.value, filter: "franchises" }, { label: t("rom.collections"), diff --git a/frontend/src/v2/views/Settings/UserInterface.vue b/frontend/src/v2/views/Settings/UserInterface.vue index 1fe8bbd6be..3256cbdc92 100644 --- a/frontend/src/v2/views/Settings/UserInterface.vue +++ b/frontend/src/v2/views/Settings/UserInterface.vue @@ -190,6 +190,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" }, ]);