autogenerate cover art

This commit is contained in:
Georges-Antoine Assi
2025-01-27 20:26:33 -05:00
parent 5dbc1aae0b
commit 10fbead892
11 changed files with 160 additions and 62 deletions

View File

@@ -8,6 +8,7 @@ Create Date: 2024-08-08 12:00:00.000000
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql as sa_pg
from utils.database import is_postgresql
# revision identifiers, used by Alembic.
@@ -18,6 +19,15 @@ depends_on = None
def upgrade() -> None:
with op.batch_alter_table("collections", schema=None) as batch_op:
batch_op.alter_column(
"roms",
new_column_name="rom_ids",
existing_type=sa.JSON().with_variant(
sa_pg.JSONB(astext_type=sa.Text()), "postgresql"
),
)
connection = op.get_bind()
if is_postgresql(connection):
connection.execute(
@@ -27,6 +37,8 @@ def upgrade() -> None:
WITH genres_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(to_jsonb(igdb_metadata ->> 'genres')) as collection_name,
'genre' as collection_type
FROM roms r
@@ -35,6 +47,8 @@ def upgrade() -> None:
franchises_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(to_jsonb(igdb_metadata->>'franchises')) as collection_name,
'franchise' as collection_type
FROM roms r
@@ -43,6 +57,8 @@ def upgrade() -> None:
collection_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(to_jsonb(igdb_metadata->>'collections')) as collection_name,
'collection' as collection_type
FROM roms r
@@ -51,6 +67,8 @@ def upgrade() -> None:
modes_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(to_jsonb(igdb_metadata->>'game_modes')) as collection_name,
'mode' as collection_type
FROM roms r
@@ -59,6 +77,8 @@ def upgrade() -> None:
companies_collection AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
jsonb_array_elements_text(to_jsonb(igdb_metadata->>'companies')) as collection_name,
'company' as collection_type
FROM roms r
@@ -70,7 +90,9 @@ def upgrade() -> None:
'Virtual collection of ' || collection_type || ' ' || collection_name AS description,
NOW() AS created_at,
NOW() AS updated_at,
array_to_json(array_agg(DISTINCT rom_id)) as roms
array_to_json(array_agg(DISTINCT rom_id)) as rom_ids,
array_to_json(array_agg(DISTINCT path_cover_s)) as path_covers_s,
array_to_json(array_agg(DISTINCT path_cover_l)) as path_covers_l
FROM (
SELECT * FROM genres_collection
UNION ALL
@@ -96,6 +118,8 @@ def upgrade() -> None:
WITH genres AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.genre) as collection_name,
'genre' as collection_type
FROM
@@ -110,6 +134,8 @@ def upgrade() -> None:
franchises AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.franchise) as collection_name,
'franchise' as collection_type
FROM
@@ -124,6 +150,8 @@ def upgrade() -> None:
collections AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.collection) as collection_name,
'collection' as collection_type
FROM
@@ -138,6 +166,8 @@ def upgrade() -> None:
modes AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.mode) as collection_name,
'mode' as collection_type
FROM
@@ -152,6 +182,8 @@ def upgrade() -> None:
companies AS (
SELECT
r.id as rom_id,
r.path_cover_s as path_cover_s,
r.path_cover_l as path_cover_l,
CONCAT(j.company) as collection_name,
'company' as collection_type
FROM
@@ -169,7 +201,9 @@ def upgrade() -> None:
CONCAT('Virtual collection of ', collection_type, ' ', collection_name) AS description,
NOW() AS created_at,
NOW() AS updated_at,
JSON_ARRAYAGG(DISTINCT rom_id) as roms
JSON_ARRAYAGG(DISTINCT rom_id) as rom_ids,
JSON_ARRAYAGG(DISTINCT path_cover_s) as path_covers_s,
JSON_ARRAYAGG(DISTINCT path_cover_l) as path_covers_l
FROM
(
SELECT * FROM genres
@@ -191,8 +225,16 @@ def upgrade() -> None:
def downgrade() -> None:
connection = op.get_bind()
with op.batch_alter_table("collections", schema=None) as batch_op:
batch_op.alter_column(
"rom_ids",
new_column_name="roms",
existing_type=sa.JSON().with_variant(
sa_pg.JSONB(astext_type=sa.Text()), "postgresql"
),
)
connection = op.get_bind()
connection.execute(
sa.text(
"""

View File

@@ -204,7 +204,7 @@ async def update_collection(
except json.JSONDecodeError as e:
raise ValueError("Invalid list for roms field in update collection") from e
except KeyError:
roms = collection.roms
roms = collection.rom_ids
cleaned_data = {
"name": data.get("name", collection.name),

View File

@@ -12,7 +12,7 @@ class CollectionSchema(BaseModel):
path_cover_small: str | None
path_cover_large: str | None
url_cover: str
roms: set[int]
rom_ids: set[int]
rom_count: int
user_id: int
user__username: str
@@ -41,8 +41,10 @@ class VirtualCollectionSchema(BaseModel):
name: str
type: str
description: str
roms: set[int]
rom_ids: set[int]
rom_count: int
path_covers_small: list[str]
path_covers_large: list[str]
class Config:
from_attributes = True

View File

@@ -602,9 +602,11 @@ async def delete_roms(
# Update collections to remove the deleted rom
collections = db_collection_handler.get_collections_by_rom_id(id)
for collection in collections:
collection.roms = {rom_id for rom_id in collection.roms if rom_id != id}
collection.rom_ids = {
rom_id for rom_id in collection.rom_ids if rom_id != id
}
db_collection_handler.update_collection(
collection.id, {"roms": collection.roms}
collection.id, {"roms": collection.rom_ids}
)
try:

View File

@@ -72,7 +72,7 @@ class DBCollectionsHandler(DBBaseHandler):
session: Session = None,
) -> Sequence[Collection]:
query = select(Collection).filter(
json_array_contains_value(Collection.roms, rom_id, session=session)
json_array_contains_value(Collection.rom_ids, rom_id, session=session)
)
if order_by is not None:
query = query.order_by(*order_by)
@@ -88,7 +88,9 @@ class DBCollectionsHandler(DBBaseHandler):
session: Session = None,
) -> Sequence[VirtualCollection]:
query = select(VirtualCollection).filter(
json_array_contains_value(VirtualCollection.roms, rom_id, session=session)
json_array_contains_value(
VirtualCollection.rom_ids, rom_id, session=session
)
)
if order_by is not None:
query = query.order_by(*order_by)

View File

@@ -69,7 +69,7 @@ class DBRomsHandler(DBBaseHandler):
.one_or_none()
)
if collection:
data = data.filter(Rom.id.in_(collection.roms))
data = data.filter(Rom.id.in_(collection.rom_ids))
if virtual_collection_id:
name, type = VirtualCollection.from_id(virtual_collection_id)
@@ -79,7 +79,7 @@ class DBRomsHandler(DBBaseHandler):
.one_or_none()
)
if v_collection:
data = data.filter(Rom.id.in_(v_collection.roms))
data = data.filter(Rom.id.in_(v_collection.rom_ids))
if search_term:
data = data.filter(

View File

@@ -25,7 +25,7 @@ class Collection(BaseModel):
Text, default="", doc="URL to cover image stored in IGDB"
)
roms: Mapped[set[int]] = mapped_column(
rom_ids: Mapped[set[int]] = mapped_column(
CustomJSON(), default=[], doc="Rom IDs that belong to this collection"
)
@@ -38,7 +38,7 @@ class Collection(BaseModel):
@property
def rom_count(self) -> int:
return len(self.roms)
return len(self.rom_ids)
@property
def fs_resources_path(self) -> str:
@@ -74,8 +74,10 @@ class VirtualCollection(BaseModel):
name: Mapped[str] = mapped_column(String(length=400), primary_key=True)
type: Mapped[str] = mapped_column(String(length=50), primary_key=True)
description: Mapped[str | None] = mapped_column(Text)
path_covers_s: Mapped[list[str]] = mapped_column(CustomJSON(), default=[])
path_covers_l: Mapped[list[str]] = mapped_column(CustomJSON(), default=[])
roms: Mapped[set[int]] = mapped_column(
rom_ids: Mapped[set[int]] = mapped_column(
CustomJSON(), default=[], doc="Rom IDs that belong to this collection"
)
@@ -92,7 +94,21 @@ class VirtualCollection(BaseModel):
@property
def rom_count(self) -> int:
return len(self.roms)
return len(self.rom_ids)
@property
def path_covers_small(self) -> list[str]:
return [
f"{FRONTEND_RESOURCES_PATH}/{cover}?ts={self.updated_at}"
for cover in self.path_covers_s
]
@property
def path_covers_large(self) -> list[str]:
return [
f"{FRONTEND_RESOURCES_PATH}/{cover}?ts={self.updated_at}"
for cover in self.path_covers_l
]
__table_args__ = (
UniqueConstraint(

View File

@@ -9,7 +9,7 @@ export type CollectionSchema = {
path_cover_small: (string | null);
path_cover_large: (string | null);
url_cover: string;
roms: Array<number>;
rom_ids: Array<number>;
rom_count: number;
user_id: number;
user__username: string;

View File

@@ -7,7 +7,9 @@ export type VirtualCollectionSchema = {
name: string;
type: string;
description: string;
roms: Array<number>;
rom_ids: Array<number>;
rom_count: number;
path_covers_small: Array<string>;
path_covers_large: Array<string>;
};

View File

@@ -2,27 +2,58 @@
import type { VirtualCollection } from "@/stores/collections";
import storeGalleryView from "@/stores/galleryView";
import { useTheme } from "vuetify";
import { computed } from "vue";
// Props
withDefaults(
const props = withDefaults(
defineProps<{
collection: VirtualCollection;
transformScale?: boolean;
showTitle?: boolean;
showRomCount?: boolean;
withLink?: boolean;
src?: string;
}>(),
{
transformScale: false,
showTitle: false,
showRomCount: false,
withLink: false,
src: "",
},
);
const theme = useTheme();
const galleryViewStore = storeGalleryView();
const getRandomCovers = computed(() => {
const largeCoverUrls = props.collection.path_covers_large || [];
const smallCoverUrls = props.collection.path_covers_small || [];
// Create a copy of the arrays to avoid mutating the original
const shuffledLarge = [...largeCoverUrls].sort(() => Math.random() - 0.5);
const shuffledSmall = [...smallCoverUrls].sort(() => Math.random() - 0.5);
console.log(shuffledLarge, shuffledSmall);
return {
large: [
shuffledLarge[0] ||
`/assets/default/cover/big_${theme.global.name.value}_collection.png`,
shuffledLarge[1] ||
`/assets/default/cover/big_${theme.global.name.value}_collection.png`,
],
small: [
shuffledSmall[0] ||
`/assets/default/cover/small_${theme.global.name.value}_collection.png`,
shuffledSmall[1] ||
`/assets/default/cover/small_${theme.global.name.value}_collection.png`,
],
};
});
const firstCover = computed(() => getRandomCovers.value.large[0]);
const secondCover = computed(() => getRandomCovers.value.large[1]);
const firstSmallCover = computed(() => getRandomCovers.value.small[0]);
const secondSmallCover = computed(() => getRandomCovers.value.small[1]);
</script>
<template>
@@ -50,42 +81,29 @@ const galleryViewStore = storeGalleryView();
<span>{{ collection.name }}</span>
</div>
</v-row>
<v-img
cover
:src="
src ||
collection.path_cover_large ||
`/assets/default/cover/big_${theme.global.name.value}_${collection.is_favorite ? 'fav' : 'collection'}.png`
"
:lazy-src="
src ||
collection.path_cover_small ||
`/assets/default/cover/small_${theme.global.name.value}_${collection.is_favorite ? 'fav' : 'collection'}.png`
"
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
>
<div class="position-absolute append-inner">
<slot name="append-inner"></slot>
</div>
<template #error>
<div
class="image-container"
:style="{ aspectRatio: galleryViewStore.defaultAspectRatioCollection }"
>
<div class="split-image first-image">
<v-img
:src="`/assets/default/cover/big_${theme.global.name.value}_collection.png`"
cover
:src="firstCover"
:lazy-src="firstSmallCover"
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
></v-img>
</template>
<template #placeholder>
<div class="d-flex align-center justify-center fill-height">
<v-progress-circular
:width="2"
:size="40"
color="romm-accent-1"
indeterminate
/>
</div>
</template>
</v-img>
/>
</div>
<div class="split-image second-image">
<v-img
cover
:src="secondCover"
:lazy-src="secondSmallCover"
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
/>
</div>
</div>
<v-chip
v-if="showRomCount"
class="bg-chip position-absolute"
@@ -98,9 +116,28 @@ const galleryViewStore = storeGalleryView();
</v-card>
</v-hover>
</template>
<style scoped>
.append-inner {
bottom: 0rem;
right: 0rem;
.image-container {
position: relative;
width: 100%;
overflow: hidden;
}
.split-image {
position: absolute;
top: 0;
width: 100%;
height: 100%;
}
.first-image {
clip-path: polygon(0 0, 100% 0, 0% 100%, 0 100%);
z-index: 1;
}
.second-image {
clip-path: polygon(0% 100%, 100% 0, 100% 100%);
z-index: 0;
}
</style>

View File

@@ -10,12 +10,7 @@ const theme = useTheme();
<template>
<v-avatar :rounded="0" :size="size">
<v-img
:src="
collection.path_cover_large ||
`/assets/default/cover/big_${theme.global.name.value}_${collection.is_favorite ? 'fav' : 'collection'}.png`
"
>
<v-img :src="collection.path_covers_large[0]">
<template #error>
<v-img
:src="`assets/default/cover/big_${theme.global.name.value}_collection.png`"