Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
390 changes: 390 additions & 0 deletions backend/alembic/versions/0063_roms_metadata_player_count.py

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/endpoints/responses/rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ class RomMetadataSchema(BaseModel):
companies: list[str]
game_modes: list[str]
age_ratings: list[str]
player_count: str
first_release_date: int | None
average_rating: float | None

Expand Down
17 changes: 17 additions & 0 deletions backend/endpoints/rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,15 @@ def get_roms(
),
),
] = None,
player_counts: Annotated[
list[str] | None,
Query(
description=(
"Associated player count. Multiple values are allowed by repeating"
" the parameter, and results that match any of the values will be returned."
),
),
] = None,
# Logic operators for multi-value filters
genres_logic: Annotated[
str,
Expand Down Expand Up @@ -382,6 +391,12 @@ def get_roms(
description="Logic operator for statuses filter: 'any' (OR) or 'all' (AND).",
),
] = "any",
player_counts_logic: Annotated[
str,
Query(
description="Logic operator for player counts filter: 'any' (OR) or 'all' (AND).",
),
] = "any",
order_by: Annotated[
str,
Query(description="Field to order results by."),
Expand Down Expand Up @@ -423,6 +438,7 @@ def get_roms(
selected_statuses=selected_statuses,
regions=regions,
languages=languages,
player_counts=player_counts,
# Logic operators
genres_logic=genres_logic,
franchises_logic=franchises_logic,
Expand All @@ -432,6 +448,7 @@ def get_roms(
regions_logic=regions_logic,
languages_logic=languages_logic,
statuses_logic=statuses_logic,
player_counts_logic=player_counts_logic,
group_by_meta_id=group_by_meta_id,
)

Expand Down
17 changes: 16 additions & 1 deletion backend/handler/database/roms_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,16 @@ def filter_by_languages(
op = json_array_contains_all if match_all else json_array_contains_any
return query.filter(op(Rom.languages, values, session=session))

def filter_by_player_counts(
self,
query: Query,
*,
session: Session,
values: Sequence[str],
match_all: bool = False,
) -> Query:
return query.filter(RomMetadata.player_count.in_(values))

@begin_session
def filter_roms(
self,
Expand All @@ -488,6 +498,7 @@ def filter_roms(
selected_statuses: Sequence[str] | None = None,
regions: Sequence[str] | None = None,
languages: Sequence[str] | None = None,
player_counts: Sequence[str] | None = None,
# Logic operators for multi-value filters
genres_logic: str = "any",
franchises_logic: str = "any",
Expand All @@ -497,6 +508,7 @@ def filter_roms(
regions_logic: str = "any",
languages_logic: str = "any",
statuses_logic: str = "any",
player_counts_logic: str = "any",
user_id: int | None = None,
session: Session = None, # type: ignore
) -> Query[Rom]:
Expand Down Expand Up @@ -656,7 +668,7 @@ def filter_roms(

# Optimize JOINs - only join tables when needed
needs_metadata_join = any(
[genres, franchises, collections, companies, age_ratings]
[genres, franchises, collections, companies, age_ratings, player_counts]
)

if needs_metadata_join:
Expand All @@ -671,6 +683,7 @@ def filter_roms(
(age_ratings, age_ratings_logic, self.filter_by_age_ratings),
(regions, regions_logic, self.filter_by_regions),
(languages, languages_logic, self.filter_by_languages),
(player_counts, player_counts_logic, self.filter_by_player_counts),
]

for values, logic, filter_func in filters_to_apply:
Expand Down Expand Up @@ -766,6 +779,7 @@ def get_roms_scalar(
selected_statuses=kwargs.get("selected_statuses", None),
regions=kwargs.get("regions", None),
languages=kwargs.get("languages", None),
player_counts=kwargs.get("player_counts", None),
# Logic operators for multi-value filters
genres_logic=kwargs.get("genres_logic", "any"),
franchises_logic=kwargs.get("franchises_logic", "any"),
Expand All @@ -775,6 +789,7 @@ def get_roms_scalar(
regions_logic=kwargs.get("regions_logic", "any"),
languages_logic=kwargs.get("languages_logic", "any"),
statuses_logic=kwargs.get("statuses_logic", "any"),
player_counts_logic=kwargs.get("player_counts_logic", "any"),
user_id=kwargs.get("user_id", None),
)
return session.scalars(roms).all()
Expand Down
8 changes: 8 additions & 0 deletions backend/handler/metadata/ss_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class SSMetadata(SSMetadataMedia):
franchises: list[str]
game_modes: list[str]
genres: list[str]
player_count: str


class SSRom(BaseRom):
Expand Down Expand Up @@ -352,6 +353,12 @@ def _get_game_modes(game: SSGame) -> list[str]:
return modes
return []

def _get_player_count(game: SSGame) -> str:
player_count = game.get("joueurs", {}).get("text")
if not player_count or str(player_count).lower() in ("null", "none"):
return "1"
return str(player_count)

return SSMetadata(
{
"ss_score": _normalize_score(game.get("note", {}).get("text", "")),
Expand All @@ -366,6 +373,7 @@ def _get_game_modes(game: SSGame) -> list[str]:
"first_release_date": _get_lowest_date(game.get("dates", [])),
"franchises": _get_franchises(game),
"game_modes": _get_game_modes(game),
"player_count": _get_player_count(game),
**extract_media_from_ss_game(rom, game),
}
)
Expand Down
1 change: 1 addition & 0 deletions backend/models/rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class RomMetadata(BaseModel):
companies: 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)
first_release_date: Mapped[int | None] = mapped_column(BigInteger(), default=None)
average_rating: Mapped[float | None] = mapped_column(default=None)

Expand Down
3 changes: 3 additions & 0 deletions backend/tests/endpoints/test_rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def test_update_rom(
"genres": '[{"id": 5, "name": "Shooter"}, {"id": 8, "name": "Platform"}, {"id": 31, "name": "Adventure"}]',
"franchises": '[{"id": 756, "name": "Metroid"}]',
"collections": '[{"id": 243, "name": "Metroid"}, {"id": 6240, "name": "Metroid Prime"}]',
"player_count": '1',
"expansions": "[]",
"dlcs": "[]",
"companies": '[{"id": 203227, "company": {"id": 70, "name": "Nintendo"}}, {"id": 203307, "company": {"id": 766, "name": "Retro Studios"}}]',
Expand Down Expand Up @@ -380,6 +381,7 @@ def test_update_raw_ss_metadata(
raw_metadata = {
"ss_score": "85",
"alternative_names": ["Test SS Game"],
"player_count": "1-4",
}

response = client.put(
Expand All @@ -396,6 +398,7 @@ def test_update_raw_ss_metadata(
assert body["ss_metadata"] is not None
assert body["ss_metadata"]["ss_score"] == "85"
assert body["ss_metadata"]["alternative_names"] == ["Test SS Game"]
assert body["ss_metadata"]["player_count"] == "1-4"

@patch.object(
LaunchboxHandler,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/__generated__/models/RomMetadataSchema.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/src/__generated__/models/RomSSMetadata.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 29 additions & 11 deletions frontend/src/components/Details/Info/GameInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const filters = [
name: t("rom.collections"),
},
{ key: "company", path: "metadatum.companies", name: t("rom.companies") },
{
key: "players",
path: "metadatum.player_count",
name: t("rom.player-count"),
},
] as const;

const dataSources = computed(() => {
Expand Down Expand Up @@ -217,17 +222,30 @@ function onFilterClick(filter: FilterType, value: string) {
<span>{{ filter.name }}</span>
</v-col>
<v-col>
<v-chip
v-for="value in get(rom, filter.path)"
:key="value"
size="small"
variant="outlined"
class="my-1 mr-2"
label
@click="onFilterClick(filter.key, value)"
>
{{ value }}
</v-chip>
<template v-if="Array.isArray(get(rom, filter.path))">
<v-chip
v-for="value in get(rom, filter.path).filter((v: string) => !!v)"
:key="value"
size="small"
variant="outlined"
class="my-1 mr-2"
label
@click="onFilterClick(filter.key, value)"
>
{{ value }}
</v-chip>
</template>
<template v-else-if="get(rom, filter.path)">
<v-chip
size="small"
variant="outlined"
class="my-1 mr-2"
label
@click="onFilterClick(filter.key, get(rom, filter.path))"
>
{{ get(rom, filter.path) }}
</v-chip>
</template>
</v-col>
</v-row>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ const {
filterLanguages,
selectedLanguages,
languagesLogic,
filterPlayerCounts,
selectedPlayerCounts,
playerCountsLogic,
} = storeToRefs(galleryFilterStore);
const { allPlatforms } = storeToRefs(platformsStore);
const emitter = inject<Emitter<Events>>("emitter");
Expand Down Expand Up @@ -150,6 +153,12 @@ const onFilterChange = debounce(
: null,
statusesLogic:
selectedStatuses.value.length > 1 ? statusesLogic.value : null,
playerCounts:
selectedPlayerCounts.value.length > 0
? selectedPlayerCounts.value.join(",")
: 1,
playerCountsLogic:
selectedPlayerCounts.value.length > 0 ? playerCountsLogic.value : 1,
}).forEach(([key, value]) => {
if (value) {
url.searchParams.set(key, value);
Expand Down Expand Up @@ -234,6 +243,14 @@ const filters = [
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setLanguagesLogic(logic),
},
{
label: t("platform.player-count"),
selected: selectedPlayerCounts,
items: filterPlayerCounts,
logic: playerCountsLogic,
setLogic: (logic: "any" | "all") =>
galleryFilterStore.setPlayerCountsLogic(logic),
},
{
label: t("platform.status"),
selected: selectedStatuses,
Expand Down Expand Up @@ -341,6 +358,10 @@ function setFilters() {
galleryFilterStore.setFilterLanguages([
...new Set(romsForFilters.flatMap((rom) => rom.languages).sort()),
]);
galleryFilterStore.setFilterPlayerCounts([
...new Set(romsForFilters.flatMap((rom) => rom.metadatum.player_count).sort(),
),
]);
// Note: filterStatuses is static and doesn't need to be set dynamically
}

Expand Down Expand Up @@ -373,6 +394,8 @@ onMounted(async () => {
languagesLogic: urlLanguagesLogic,
statuses: urlStatuses,
statusesLogic: urlStatusesLogic,
playerCounts: urlPlayerCounts,
playerCountsLogic: urlPlayerCountsLogic,
} = router.currentRoute.value.query;

// Check for query params to set filters
Expand Down Expand Up @@ -535,6 +558,14 @@ onMounted(async () => {
}
}

if (urlPlayerCounts !== undefined) {
const playerCounts = (urlPlayerCounts as string).split(",").filter((pc) => pc.trim());
galleryFilterStore.setSelectedFilterPlayerCounts(playerCounts);
if (urlPlayerCountsLogic !== undefined) {
galleryFilterStore.setPlayerCountsLogic(urlPlayerCountsLogic as "any" | "all");
}
}

// Check if search term is set in the URL (empty string is ok)
const freshSearch = urlSearch !== undefined && urlSearch !== searchTerm.value;
if (freshSearch) {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/locales/en_US/platform.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"no-firmware-found": "No firmware found",
"old-horizontal-cases": "Old horizontal cases",
"old-squared-cases": "Old squared cases",
"player-count": "Player count",
"region": "Region",
"removing-platform-1": "Removing platform",
"removing-platform-2": "] from RomM. Do you confirm?",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/locales/en_US/rom.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"no-states-found": "No states found",
"now-playing": "Now playing",
"personal": "Personal",
"player-count": "Players",
"public-notes": "Public notes",
"rating": "Rating",
"refresh-metadata": "Refresh metadata",
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/services/api/rom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export interface GetRomsParams {
selectedAgeRatings?: string[] | null;
selectedRegions?: string[] | null;
selectedLanguages?: string[] | null;
selectedPlayerCounts?: string[] | null;
selectedStatuses?: string[] | null;
// Logic operators for multi-value filters
genresLogic?: string | null;
Expand All @@ -97,6 +98,7 @@ export interface GetRomsParams {
regionsLogic?: string | null;
languagesLogic?: string | null;
statusesLogic?: string | null;
playerCountsLogic?: string | null;
}

async function getRoms({
Expand Down Expand Up @@ -124,6 +126,7 @@ async function getRoms({
selectedAgeRatings = null,
selectedRegions = null,
selectedLanguages = null,
selectedPlayerCounts = null,
selectedStatuses = null,
// Logic operators
genresLogic = null,
Expand All @@ -134,6 +137,7 @@ async function getRoms({
regionsLogic = null,
languagesLogic = null,
statusesLogic = null,
playerCountsLogic = null,
}: GetRomsParams): Promise<{ data: GetRomsResponse }> {
const params = {
platform_ids:
Expand Down Expand Up @@ -177,6 +181,10 @@ async function getRoms({
selectedLanguages && selectedLanguages.length > 0
? selectedLanguages
: undefined,
player_counts:
selectedPlayerCounts && selectedPlayerCounts.length > 0
? selectedPlayerCounts
: undefined,
// Logic operators
genres_logic:
selectedGenres && selectedGenres.length > 1
Expand Down Expand Up @@ -210,6 +218,10 @@ async function getRoms({
selectedStatuses && selectedStatuses.length > 1
? statusesLogic || "any"
: undefined,
player_counts_logic:
selectedPlayerCounts && selectedPlayerCounts.length > 1
? playerCountsLogic || "any"
: undefined,
...(filterMatched !== null ? { matched: filterMatched } : {}),
...(filterFavorites !== null ? { favorite: filterFavorites } : {}),
...(filterDuplicates !== null ? { duplicate: filterDuplicates } : {}),
Expand Down
Loading