Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
159 changes: 159 additions & 0 deletions backend/alembic/versions/0110_walkthrough_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Add walkthrough document support.

Adds the WALKTHROUGH value to the rom_files.category enum, plus two tables:
- rom_file_doc_meta: provenance sidecar for document-category files
- rom_file_user: per-user reading progress for document-category files

Revision ID: 0110_walkthrough_docs
Revises: 0109_rom_files_category_index
Create Date: 2026-07-18 00:00:00.000000

"""

import sqlalchemy as sa
from alembic import op # type: ignore[attr-defined]

from utils.database import is_postgresql

# revision identifiers, used by Alembic.
revision = "0110_walkthrough_docs"
down_revision = "0109_rom_files_category_index"
branch_labels = None
depends_on = None

# Full enum member set including WALKTHROUGH, in model declaration order.
ROM_FILE_CATEGORY_VALUES = (
"GAME",
"DLC",
"HACK",
"MANUAL",
"WALKTHROUGH",
"PATCH",
"UPDATE",
"MOD",
"DEMO",
"TRANSLATION",
"PROTOTYPE",
"CHEAT",
"SOUNDTRACK",
"SCREENSHOT",
)

# The pre-walkthrough set, used to narrow the enum back on downgrade.
ROM_FILE_CATEGORY_VALUES_PRE = tuple(
v for v in ROM_FILE_CATEGORY_VALUES if v != "WALKTHROUGH"
)


def upgrade() -> None:
connection = op.get_bind()

# 1. Extend the rom_files.category enum with WALKTHROUGH.
if is_postgresql(connection):
# `ALTER TYPE ... ADD VALUE` must run outside a transaction in PostgreSQL.
with op.get_context().autocommit_block():
op.execute(
"ALTER TYPE romfilecategory ADD VALUE IF NOT EXISTS 'WALKTHROUGH'"
)
else:
with op.batch_alter_table("rom_files", schema=None) as batch_op:
batch_op.alter_column(
"category",
type_=sa.Enum(*ROM_FILE_CATEGORY_VALUES, name="romfilecategory"),
nullable=True,
)

# 2. Provenance sidecar for document-category files.
op.create_table(
"rom_file_doc_meta",
sa.Column("rom_file_id", sa.Integer(), nullable=False),
sa.Column("rom_id", sa.Integer(), nullable=False),
sa.Column(
"source",
sa.Enum("UPLOAD", "GAMEFAQS", "SCRAPER", name="docsource"),
nullable=False,
),
sa.Column("source_url", sa.Text(), nullable=True),
sa.Column("author", sa.String(length=512), nullable=True),
sa.Column("title", sa.String(length=512), nullable=True),
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
nullable=False,
server_default=sa.text("CURRENT_TIMESTAMP"),
),
sa.Column(
"updated_at",
sa.TIMESTAMP(timezone=True),
nullable=False,
server_default=sa.text("CURRENT_TIMESTAMP"),
),
sa.ForeignKeyConstraint(["rom_file_id"], ["rom_files.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["rom_id"], ["roms.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("rom_file_id"),
if_not_exists=True,
)
with op.batch_alter_table("rom_file_doc_meta", schema=None) as batch_op:
batch_op.create_index(
"idx_rom_file_doc_meta_rom_id",
["rom_id"],
unique=False,
if_not_exists=True,
)

# 3. Per-user reading progress for document-category files.
op.create_table(
"rom_file_user",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("rom_file_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("progress", sa.Float(), nullable=False, server_default=sa.text("0")),
sa.Column("last_page", sa.Integer(), nullable=True),
sa.Column("finished", sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column("last_read_at", sa.TIMESTAMP(timezone=True), nullable=True),
sa.Column(
"created_at",
sa.TIMESTAMP(timezone=True),
nullable=False,
server_default=sa.text("CURRENT_TIMESTAMP"),
),
sa.Column(
"updated_at",
sa.TIMESTAMP(timezone=True),
nullable=False,
server_default=sa.text("CURRENT_TIMESTAMP"),
),
sa.ForeignKeyConstraint(["rom_file_id"], ["rom_files.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("rom_file_id", "user_id", name="unique_rom_file_user"),
if_not_exists=True,
)
with op.batch_alter_table("rom_file_user", schema=None) as batch_op:
batch_op.create_index(
"idx_rom_file_user",
["rom_file_id", "user_id"],
unique=False,
if_not_exists=True,
)


def downgrade() -> None:
connection = op.get_bind()

op.drop_table("rom_file_user", if_exists=True)
op.drop_table("rom_file_doc_meta", if_exists=True)

# PostgreSQL cannot drop an enum value, so leave WALKTHROUGH in place there.
# The docsource type is dropped with its table on PG; drop it explicitly in
# case create_table left it behind.
if is_postgresql(connection):
op.execute("DROP TYPE IF EXISTS docsource")
return

with op.batch_alter_table("rom_files", schema=None) as batch_op:
batch_op.alter_column(
"category",
type_=sa.Enum(*ROM_FILE_CATEGORY_VALUES_PRE, name="romfilecategory"),
nullable=True,
)
30 changes: 29 additions & 1 deletion backend/endpoints/responses/rom.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@
from handler.metadata.ra_handler import RAMetadata
from handler.metadata.ss_handler import SSMetadata
from models.collection import Collection, SmartCollection
from models.rom import Rom, RomArchiveMember, RomFile, RomFileCategory, RomUserStatus
from models.rom import (
DocSource,
Rom,
RomArchiveMember,
RomFile,
RomFileCategory,
RomUserStatus,
)

from .base import BaseModel, UTCDatetime

Expand Down Expand Up @@ -173,6 +180,26 @@ class TrackMetaSchema(BaseModel):
cover_path: str | None = None


class DocMetaSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)

source: DocSource
source_url: str | None = None
author: str | None = None
title: str | None = None


class RomFileUserSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)

rom_file_id: int
user_id: int
progress: float
last_page: int | None = None
finished: bool = False
last_read_at: UTCDatetime | None = None


class RomFileSchema(BaseModel):
model_config = ConfigDict(from_attributes=True)

Expand All @@ -194,6 +221,7 @@ class RomFileSchema(BaseModel):
archive_members: list[RomArchiveMember] | None
category: RomFileCategory | None
track_meta: TrackMetaSchema | None = None
doc_meta: DocMetaSchema | None = None

@model_validator(mode="after")
def default_category_for_non_nested(self) -> RomFileSchema:
Expand Down
2 changes: 2 additions & 0 deletions backend/endpoints/roms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
from .screenshot import router as screenshot_router
from .soundtrack import router as soundtrack_router
from .upload import router as upload_router
from .walkthrough import router as walkthrough_router

router = APIRouter(
prefix="/roms",
Expand All @@ -113,6 +114,7 @@
router.include_router(upload_router)
router.include_router(files_router)
router.include_router(manual_router)
router.include_router(walkthrough_router)
router.include_router(soundtrack_router)
router.include_router(screenshot_router)
router.include_router(notes_router)
Expand Down
Loading
Loading