[ROMM-911] Fix playing multi disc games in emujs

This commit is contained in:
Georges-Antoine Assi
2025-01-26 23:58:58 -05:00
parent 06172b8ec1
commit 0a57ceeddf
14 changed files with 117 additions and 23 deletions

View File

@@ -194,7 +194,6 @@ async def head_rom_content(
request: Request,
id: int,
file_name: str,
file_ids: list[int] | None = None,
):
"""Head rom content endpoint
@@ -213,13 +212,14 @@ async def head_rom_content(
if not rom:
raise RomNotFoundInDatabaseException(id)
file_ids = file_ids or []
file_ids = request.query_params.get("file_ids") or ""
file_ids = [int(f) for f in file_ids.split(",") if f]
files = db_rom_handler.get_rom_files(rom.id)
files = [f for f in files if f.id in file_ids or not file_ids]
if not rom.multi:
# Serve the file directly in development mode for emulatorjs
if DEV_MODE:
# Serve the file directly in development mode for emulatorjs
if DEV_MODE:
if not rom.multi:
rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}"
return FileResponse(
path=rom_path,
@@ -231,6 +231,28 @@ async def head_rom_content(
},
)
if len(files) == 1:
file = files[0]
rom_path = f"{LIBRARY_BASE_PATH}/{file.full_path}"
return FileResponse(
path=rom_path,
filename=file.file_name,
headers={
"Content-Disposition": f'attachment; filename="{quote(file.file_name)}"',
"Content-Type": "application/octet-stream",
"Content-Length": str(file.file_size_bytes),
},
)
return Response(
headers={
"Content-Type": "application/zip",
"Content-Disposition": f'attachment; filename="{quote(file_name)}.zip"',
},
)
# Otherwise proxy through nginx
if not rom.multi:
return FileRedirectResponse(
download_path=Path(f"/library/{rom.full_path}"),
filename=rom.fs_name,
@@ -258,7 +280,6 @@ async def get_rom_content(
request: Request,
id: int,
file_name: str,
file_ids: list[int] | None = None,
):
"""Download rom endpoint (one single file or multiple zipped files for multi-part roms)
@@ -266,7 +287,6 @@ async def get_rom_content(
request (Request): Fastapi Request object
id (int): Rom internal id
file_name: Zip file output name
file_ids (list[int]): List of file ids to download for multi-part roms
Returns:
FileResponse: Returns one file for single file roms
@@ -281,15 +301,16 @@ async def get_rom_content(
if not rom:
raise RomNotFoundInDatabaseException(id)
file_ids = file_ids or []
file_ids = request.query_params.get("file_ids") or ""
file_ids = [int(f) for f in file_ids.split(",") if f]
files = db_rom_handler.get_rom_files(rom.id)
files = [f for f in files if f.id in file_ids or not file_ids]
log.info(f"User {current_username} is downloading {rom.fs_name}")
if not rom.multi:
# Serve the file directly in development mode for emulatorjs
if DEV_MODE:
# Serve the file directly in development mode for emulatorjs
if DEV_MODE:
if not rom.multi:
rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}"
return FileResponse(
path=rom_path,
@@ -301,6 +322,36 @@ async def get_rom_content(
},
)
if len(files) == 1:
file = files[0]
rom_path = f"{LIBRARY_BASE_PATH}/{file.full_path}"
return FileResponse(
path=rom_path,
filename=file.file_name,
headers={
"Content-Disposition": f'attachment; filename="{quote(file.file_name)}"',
"Content-Type": "application/octet-stream",
"Content-Length": str(file.file_size_bytes),
},
)
content_lines = [
ZipContentLine(
crc32=f.crc_hash,
size_bytes=(await Path(LIBRARY_BASE_PATH, f.full_path).stat()).st_size,
encoded_location=quote(f"{LIBRARY_BASE_PATH}/{f.full_path}"),
filename=f.full_path.replace(rom.full_path, ""),
)
for f in files
]
return ZipResponse(
content_lines=content_lines,
filename=f"{quote(file_name)}.zip",
)
# Otherwise proxy through nginx
if not rom.multi:
return FileRedirectResponse(
download_path=Path(f"/library/{rom.full_path}"),
filename=rom.fs_name,

View File

@@ -3,6 +3,7 @@
"platforms": "Plattformen",
"platforms-n": "{n} Plattform | {n} Plattformen",
"firmware": "Firmware",
"core": "Core",
"games-n": "{n} Spiel | {n} Spiele",
"collection": "Sammlung",
"collections": "Sammlungen",

View File

@@ -3,6 +3,7 @@
"platforms": "Platforms",
"platforms-n": "{n} Platform | {n} Platforms",
"firmware": "Firmware",
"core": "Core",
"games-n": "{n} Game | {n} Games",
"collection": "Collection",
"collections": "Collections",

View File

@@ -3,6 +3,7 @@
"platforms": "Platforms",
"platforms-n": "{n} Platform | {n} Platforms",
"firmware": "Firmware",
"core": "Core",
"games-n": "{n} Game | {n} Games",
"collection": "Collection",
"collections": "Collections",

View File

@@ -3,6 +3,7 @@
"platforms": "Plataformas",
"platforms-n": "{n} Plataforma | {n} Plataformas",
"firmware": "Firmware",
"core": "Núcleo",
"games-n": "{n} Juego | {n} Juegos",
"collection": "Colección",
"collections": "Colecciones",

View File

@@ -2,7 +2,8 @@
"platform": "Plateforme",
"platforms": "Plateformes",
"platforms-n": "{n} Plateforme | {n} Plateformes",
"firmware": "Firmware",
"firmware": "Micrologiciel",
"core": "Noyau",
"games-n": "{n} Jeu | {n} Jeux",
"collection": "Collection",
"collections": "Collections",

View File

@@ -3,6 +3,7 @@
"platforms": "플랫폼",
"platforms-n": "{n}가지 플랫폼 | {n}가지 플랫폼",
"firmware": "펌웨어",
"core": "코어",
"games-n": "게임 {n}개 | 게임 {n}개",
"collection": "모음집",
"collections": "모음집",

View File

@@ -3,6 +3,7 @@
"platforms": "Plataformas",
"platforms-n": "{n} Plataforma | {n} Plataformas",
"firmware": "Firmware",
"core": "Núcleo",
"games-n": "{n} Jogo | {n} Jogos",
"collection": "Coleção",
"collections": "Coleções",

View File

@@ -2,7 +2,8 @@
"platform": "Платформа",
"platforms": "Платформы",
"platforms-n": "{n} Платформа | {n} Платформы",
"firmware": "Firmware",
"firmware": "Прошивка",
"core": "Ядро",
"games-n": "{n} Игра | {n} Игры",
"collection": "Коллекция",
"collections": "Коллекции",

View File

@@ -3,6 +3,7 @@
"platforms": "平台",
"platforms-n": "{n} 平台 | {n} 平台",
"firmware": "固件",
"core": "核心",
"games-n": "{n} 游戏 | {n} 游戏",
"collection": "收藏",
"collections": "收藏",

View File

@@ -95,7 +95,9 @@ export function getDownloadPath({
}) {
const queryParams = new URLSearchParams();
if (fileIDs.length > 0) {
fileIDs.forEach((fileId) => queryParams.append("files", fileId.toString()));
fileIDs.forEach((fileId) =>
queryParams.append("file_ids", fileId.toString()),
);
}
return `/api/roms/${rom.id}/content/${rom.fs_name}?${queryParams.toString()}`;
}

View File

@@ -4,7 +4,6 @@ import RomListItem from "@/components/common/Game/ListItem.vue";
import firmwareApi from "@/services/api/firmware";
import romApi from "@/services/api/rom";
import storeGalleryView from "@/stores/galleryView";
import storeHeartbeat from "@/stores/heartbeat";
import type { DetailedRom } from "@/stores/roms";
import { formatBytes, formatTimestamp, getSupportedEJSCores } from "@/utils";
import Player from "@/views/Player/EmulatorJS/Player.vue";
@@ -21,13 +20,13 @@ const { t } = useI18n();
const route = useRoute();
const galleryViewStore = storeGalleryView();
const { defaultAspectRatioScreenshot } = storeToRefs(galleryViewStore);
const heartbeat = storeHeartbeat();
const rom = ref<DetailedRom | null>(null);
const firmwareOptions = ref<FirmwareSchema[]>([]);
const biosRef = ref<FirmwareSchema | null>(null);
const saveRef = ref<SaveSchema | null>(null);
const stateRef = ref<StateSchema | null>(null);
const coreRef = ref<string | null>(null);
const discRef = ref<number | null>(null);
const supportedCores = ref<string[]>([]);
const gameRunning = ref(false);
const storedFSOP = localStorage.getItem("fullScreenOnPlay");
@@ -117,6 +116,11 @@ onMounted(async () => {
// Otherwise auto select first supported core
coreRef.value = supportedCores.value[0];
}
const storedDisc = localStorage.getItem(`player:${rom.value.id}:disc`);
if (storedDisc) {
discRef.value = parseInt(storedDisc);
}
});
</script>
@@ -138,6 +142,7 @@ onMounted(async () => {
:save="saveRef"
:bios="biosRef"
:core="coreRef"
:disc="discRef"
/>
</v-col>
@@ -157,6 +162,22 @@ onMounted(async () => {
<v-divider class="my-4" />
<rom-list-item :rom="rom" with-filename with-size />
<v-divider class="my-4" />
<v-select
v-if="rom.multi"
v-model="discRef"
class="my-1"
hide-details
rounded="0"
variant="outlined"
clearable
:label="t('rom.file')"
:items="
rom.files.map((f) => ({
title: f.file_name,
value: f.id,
}))
"
/>
<v-select
v-if="supportedCores.length > 1"
:disabled="gameRunning"
@@ -166,7 +187,7 @@ onMounted(async () => {
hide-details
variant="outlined"
clearable
label="Core"
:label="t('common.core')"
:items="
supportedCores.map((c) => ({
title: c,

View File

@@ -8,6 +8,7 @@ import {
areThreadsRequiredForEJSCore,
getSupportedEJSCores,
getControlSchemeForPlatform,
getDownloadPath,
} from "@/utils";
import { onBeforeUnmount, onMounted, ref } from "vue";
@@ -17,6 +18,7 @@ const props = defineProps<{
state: StateSchema | null;
bios: FirmwareSchema | null;
core: string | null;
disc: number | null;
}>();
const romRef = ref<DetailedRom>(props.rom);
const saveRef = ref<SaveSchema | null>(props.save);
@@ -62,7 +64,10 @@ window.EJS_controlScheme = getControlSchemeForPlatform(
);
window.EJS_threads = areThreadsRequiredForEJSCore(window.EJS_core);
window.EJS_gameID = romRef.value.id;
window.EJS_gameUrl = `/api/roms/${romRef.value.id}/content/${romRef.value.fs_name}`;
window.EJS_gameUrl = getDownloadPath({
rom: romRef.value,
fileIDs: props.disc ? [props.disc] : [],
});
window.EJS_biosUrl = props.bios
? `/api/firmware/${props.bios.id}/content/${props.bios.file_name}`
: "";
@@ -114,6 +119,12 @@ onMounted(() => {
} else {
localStorage.removeItem(`player:${props.rom.platform_slug}:core`);
}
if (props.disc) {
localStorage.setItem(`player:${props.rom.id}:disc`, props.disc.toString());
} else {
localStorage.removeItem(`player:${props.rom.id}:disc`);
}
});
function buildStateName(): string {

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import RomListItem from "@/components/common/Game/ListItem.vue";
import romApi from "@/services/api/rom";
import storeHeartbeat from "@/stores/heartbeat";
import storeGalleryView from "@/stores/galleryView";
import type { DetailedRom } from "@/stores/roms";
import { isNull } from "lodash";
@@ -9,6 +8,7 @@ import { storeToRefs } from "pinia";
import { nextTick, onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { getDownloadPath } from "@/utils";
const RUFFLE_VERSION = "0.1.0-nightly.2024.12.28";
@@ -16,7 +16,6 @@ const RUFFLE_VERSION = "0.1.0-nightly.2024.12.28";
const { t } = useI18n();
const route = useRoute();
const galleryViewStore = storeGalleryView();
const heartbeat = storeHeartbeat();
const { defaultAspectRatioScreenshot } = storeToRefs(galleryViewStore);
const rom = ref<DetailedRom | null>(null);
const gameRunning = ref(false);
@@ -36,6 +35,8 @@ function onPlay() {
gameRunning.value = true;
nextTick(() => {
if (!rom.value) return;
const ruffle = window.RufflePlayer.newest();
const player = ruffle.createPlayer();
const container = document.getElementById("game");
@@ -46,7 +47,7 @@ function onPlay() {
backgroundColor: "#0D1117",
openUrlMode: "confirm",
publicPath: "/assets/ruffle/",
url: `/api/roms/${rom.value?.id}/content/${rom.value?.fs_name}`,
url: getDownloadPath({ rom: rom.value }),
});
player.style.width = "100%";
player.style.height = "100%";
@@ -73,8 +74,7 @@ onMounted(async () => {
script.onerror = () => {
const fallbackScript = document.createElement("script");
fallbackScript.src =
"https://unpkg.com/@ruffle-rs/ruffle@${RUFFLE_VERSION}/ruffle.js";
fallbackScript.src = `https://unpkg.com/@ruffle-rs/ruffle@${RUFFLE_VERSION}/ruffle.js`;
document.body.appendChild(fallbackScript);
};