mirror of
https://github.com/rommapp/romm.git
synced 2026-02-19 07:50:57 +01:00
autogenerate cover art
This commit is contained in:
@@ -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(
|
||||
"""
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`"
|
||||
|
||||
Reference in New Issue
Block a user