-
-
Notifications
You must be signed in to change notification settings - Fork 419
Add soundtrack support and revamp manual handling under a unified Media tab #3283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
916e894
9f7435c
b7c998d
bd38edd
b6fcffe
f9737a1
8b7bb80
2957276
b4cfc51
aa68888
b76c015
72395fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| """Update rom_file.category column enum to include soundtracks, and add | ||
| audio_meta JSON column for tag/duration/cover metadata. | ||
|
|
||
| Revision ID: 0080_rom_category_soundtrack | ||
| Revises: 0079_add_rom_files_rom_id_index | ||
| Create Date: 2026-04-17 00:00:00.000000 | ||
|
|
||
| """ | ||
|
|
||
| import sqlalchemy as sa | ||
| from alembic import op | ||
|
|
||
| from utils.database import CustomJSON, is_postgresql | ||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision = "0080_rom_category_soundtrack" | ||
| down_revision = "0079_add_rom_files_rom_id_index" | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| connection = op.get_bind() | ||
|
|
||
| if is_postgresql(connection): | ||
| # `ALTER TYPE ... ADD VALUE` must run outside a transaction in PostgreSQL. | ||
| # autocommit_block breaks out of alembic's wrapping transaction for | ||
| # exactly this operation. | ||
| with op.get_context().autocommit_block(): | ||
| op.execute( | ||
| "ALTER TYPE romfilecategory ADD VALUE IF NOT EXISTS 'SOUNDTRACK'" | ||
| ) | ||
|
|
||
| # audio_meta column (added separately so the enum change sticks even | ||
| # if the JSON column already exists from a partial prior run). | ||
| op.add_column( | ||
| "rom_files", | ||
| sa.Column("audio_meta", CustomJSON(), nullable=True), | ||
| if_not_exists=True, | ||
| ) | ||
| else: | ||
| rom_file_category_enum = sa.Enum( | ||
| "GAME", | ||
| "DLC", | ||
| "HACK", | ||
| "MANUAL", | ||
| "PATCH", | ||
| "UPDATE", | ||
| "MOD", | ||
| "DEMO", | ||
| "TRANSLATION", | ||
| "PROTOTYPE", | ||
| "CHEAT", | ||
| "SOUNDTRACK", | ||
| name="romfilecategory", | ||
| ) | ||
| with op.batch_alter_table("rom_files", schema=None) as batch_op: | ||
| batch_op.alter_column( | ||
| "category", type_=rom_file_category_enum, nullable=True | ||
| ) | ||
| batch_op.add_column( | ||
| sa.Column("audio_meta", CustomJSON(), nullable=True), | ||
| if_not_exists=True, | ||
| ) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| # PostgreSQL cannot remove enum values, so on downgrade we only drop the | ||
| # audio_meta column and leave SOUNDTRACK in the enum. Any rows that still | ||
| # hold the new value keep working; a re-upgrade is a no-op. | ||
| with op.batch_alter_table("rom_files", schema=None) as batch_op: | ||
| batch_op.drop_column("audio_meta", if_exists=True) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import mimetypes | ||
| import os | ||
| from typing import Annotated | ||
| from urllib.parse import quote | ||
|
|
||
| from anyio import Path | ||
| from fastapi import HTTPException | ||
|
|
@@ -16,11 +17,28 @@ | |
| from logger.formatter import BLUE | ||
| from logger.formatter import highlight as hl | ||
| from logger.logger import log | ||
| from models.rom import RomFileCategory | ||
| from utils.nginx import FileRedirectResponse | ||
| from utils.router import APIRouter | ||
|
|
||
| router = APIRouter() | ||
|
|
||
| _AUDIO_MIME_OVERRIDES = { | ||
| ".flac": "audio/flac", | ||
| ".opus": "audio/ogg", | ||
| ".m4a": "audio/mp4", | ||
| ".oga": "audio/ogg", | ||
| ".ogg": "audio/ogg", | ||
| } | ||
|
|
||
|
|
||
| def _guess_media_type(file_name: str) -> str: | ||
| ext = os.path.splitext(file_name)[1].lower() | ||
| if ext in _AUDIO_MIME_OVERRIDES: | ||
| return _AUDIO_MIME_OVERRIDES[ext] | ||
| guessed, _ = mimetypes.guess_type(file_name) | ||
| return guessed or "application/octet-stream" | ||
|
|
||
|
|
||
| @protected_route( | ||
| router.get, | ||
|
|
@@ -70,20 +88,27 @@ async def get_romfile_content( | |
|
|
||
| log.info(f"User {hl(current_username, color=BLUE)} is downloading {hl(file_name)}") | ||
|
|
||
| is_audio = file.category == RomFileCategory.SOUNDTRACK | ||
| media_type = ( | ||
| _guess_media_type(file_name) if is_audio else "application/octet-stream" | ||
| ) | ||
| disposition = "inline" if is_audio else "attachment" | ||
|
|
||
| # Serve the file directly in development mode for emulatorjs | ||
| if DEV_MODE: | ||
| rom_path = fs_rom_handler.validate_path(file.full_path) | ||
| # Starlette sets Content-Length and honors Range natively — inline | ||
| # disposition lets <audio> seek via Range requests. | ||
| return FileResponse( | ||
| path=rom_path, | ||
| filename=file_name, | ||
| headers={ | ||
| "Content-Disposition": f"attachment; filename*=UTF-8''{quote(file_name)}; filename=\"{quote(file_name)}\"", | ||
| "Content-Type": "application/octet-stream", | ||
| "Content-Length": str(file.file_size_bytes), | ||
| }, | ||
| media_type=media_type, | ||
| content_disposition_type=disposition, | ||
| ) | ||
|
Comment on lines
89
to
107
|
||
|
|
||
| # Otherwise proxy through nginx | ||
| # Otherwise proxy through nginx (which parses Range itself via X-Accel-Redirect) | ||
| return FileRedirectResponse( | ||
| download_path=Path(f"/library/{file.full_path}"), | ||
| disposition=disposition, | ||
| media_type=media_type, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe these two should be moved to roms/soundtrack.py?