self code cleanup

This commit is contained in:
Georges-Antoine Assi
2025-10-23 10:36:08 -04:00
parent 699dee4bce
commit f919951a22
9 changed files with 108 additions and 335 deletions

View File

@@ -29,12 +29,8 @@ async def export_gamelist(
platform_ids: Annotated[
List[int], Query(description="List of platform IDs to export")
],
rom_ids: Annotated[
Optional[List[int]],
Query(description="Optional list of specific ROM IDs to export"),
] = None,
) -> Response:
"""Export platforms/ROMs to gamelist.xml format"""
"""Export platforms/ROMs to gamelist.xml format and write to platform directories"""
if not platform_ids:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
@@ -43,44 +39,26 @@ async def export_gamelist(
try:
exporter = GamelistExporter()
files_written = []
# If only one platform, return single XML
if len(platform_ids) == 1:
platform_id = platform_ids[0]
xml_content = exporter.export_platform(platform_id, rom_ids, request)
# Export each platform to its respective directory
for platform_id in platform_ids:
success = exporter.export_platform_to_file(platform_id, request)
if success:
files_written.append(f"gamelist_{platform_id}.xml")
else:
log.warning(f"Failed to write gamelist for platform {platform_id}")
log.info(
f"Exported gamelist for platform {hl(str(platform_id), color=BLUE)} "
f"with {hl(str(len(rom_ids) if rom_ids else 'all'), color=BLUE)} ROMs"
)
return Response(
content=xml_content,
media_type="application/xml",
headers={
"Content-Disposition": f"attachment; filename=gamelist_{platform_id}.xml"
},
)
else:
# Multiple platforms - return as zip or individual files
# For now, return the first platform's XML
# TODO: Implement zip export for multiple platforms
platform_id = platform_ids[0]
xml_content = exporter.export_platform(platform_id, rom_ids, request)
log.info(
f"Exported gamelist for platform {hl(str(platform_id), color=BLUE)} "
f"(first of {len(platform_ids)} platforms)"
)
return Response(
content=xml_content,
media_type="application/xml",
headers={
"Content-Disposition": f"attachment; filename=gamelist_{platform_id}.xml"
},
if not files_written:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to write any gamelist files",
)
log.info(
f"Exported gamelist for {hl(str(len(files_written)), color=BLUE)} platform(s)"
)
return Response(status_code=status.HTTP_200_OK)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
except Exception as e:
@@ -89,103 +67,3 @@ async def export_gamelist(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to export gamelist",
) from e
@protected_route(router.post, "/export/platform/{platform_id}", [Scope.ROMS_READ])
async def export_platform_gamelist(
request: Request,
platform_id: int,
rom_ids: Annotated[
Optional[List[int]],
Query(description="Optional list of specific ROM IDs to export"),
] = None,
) -> Response:
"""Export a specific platform to gamelist.xml format"""
try:
exporter = GamelistExporter()
xml_content = exporter.export_platform(platform_id, rom_ids, request)
log.info(
f"Exported gamelist for platform {hl(str(platform_id), color=BLUE)} "
f"with {hl(str(len(rom_ids) if rom_ids else 'all'), color=BLUE)} ROMs"
)
return Response(
content=xml_content,
media_type="application/xml",
headers={
"Content-Disposition": f"attachment; filename=gamelist_{platform_id}.xml"
},
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
except Exception as e:
log.error(f"Failed to export platform gamelist: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to export platform gamelist",
) from e
@protected_route(
router.get, "/export/platform/{platform_id}/preview", [Scope.ROMS_READ]
)
async def preview_platform_gamelist(
request: Request,
platform_id: int,
rom_ids: Annotated[
Optional[List[int]],
Query(description="Optional list of specific ROM IDs to preview"),
] = None,
) -> dict:
"""Preview gamelist export for a platform (returns metadata without full XML)"""
try:
from handler.database import db_platform_handler, db_rom_handler
platform = db_platform_handler.get_platform(platform_id)
if not platform:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Platform with ID {platform_id} not found",
)
# Get ROMs for the platform
if rom_ids:
roms = [db_rom_handler.get_rom(rom_id) for rom_id in rom_ids]
roms = [rom for rom in roms if rom and rom.platform_id == platform_id]
else:
roms = db_rom_handler.get_roms_scalar(platform_id=platform_id)
# Return preview metadata
return {
"platform": {
"id": platform.id,
"name": platform.name,
"fs_slug": platform.fs_slug,
},
"rom_count": len(roms),
"roms": [
{
"id": rom.id,
"name": rom.name or rom.fs_name,
"fs_name": rom.fs_name,
"has_cover": bool(rom.url_cover),
"has_screenshots": bool(rom.url_screenshots),
"has_summary": bool(rom.summary),
}
for rom in roms
if rom and not rom.missing_from_fs
],
}
except HTTPException:
raise
except Exception as e:
log.error(f"Failed to preview platform gamelist: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to preview platform gamelist",
) from e

View File

@@ -8,25 +8,21 @@ from xml.etree.ElementTree import ( # trunk-ignore(bandit/B405)
tostring,
)
from config import YOUTUBE_BASE_URL
from config import FRONTEND_RESOURCES_PATH, YOUTUBE_BASE_URL
from handler.database import db_platform_handler, db_rom_handler
from handler.filesystem import fs_platform_handler
from logger.logger import log
from models.rom import Rom
class GamelistExporter:
"""Export RomM collections to EmulationStation gamelist.xml format"""
def __init__(self):
self.base_path = "/romm/library" # Base path for ROM files
"""Export RomM collections to ES-DE gamelist.xml format"""
def _format_release_date(self, date_str: int) -> str:
"""Format release date to YYYYMMDDTHHMMSS format"""
return f"{date_str // 10000:04d}{date_str % 10000:02d}T000000"
def _create_game_element(
self, rom: Rom, platform_dir: str, request=None
) -> Element:
def _create_game_element(self, rom: Rom, request=None) -> Element:
"""Create a <game> element for a ROM"""
game = Element("game")
@@ -47,14 +43,26 @@ class GamelistExporter:
SubElement(game, "desc").text = rom.summary
# Media files
if rom.url_cover:
SubElement(game, "image").text = rom.url_cover
if rom.path_cover_large:
SubElement(game, "cover").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.path_cover_large}"
)
if rom.youtube_video_id:
SubElement(game, "video").text = (
f"{YOUTUBE_BASE_URL}/embed/{rom.youtube_video_id}"
)
if rom.path_screenshots:
SubElement(game, "screenshot").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.path_screenshots[0]}"
)
if rom.path_manual:
SubElement(game, "manual").text = (
f"{FRONTEND_RESOURCES_PATH}/{rom.path_manual}"
)
# Additional metadata
if rom.metadatum.companies and len(rom.metadatum.companies) > 0:
SubElement(game, "developer").text = rom.metadatum.companies[0]
@@ -65,9 +73,6 @@ class GamelistExporter:
if rom.metadatum.genres and len(rom.metadatum.genres) > 0:
SubElement(game, "genre").text = rom.metadatum.genres[0]
if rom.gamelist_metadata and rom.gamelist_metadata["players"]:
SubElement(game, "players").text = rom.gamelist_metadata["players"]
if rom.languages and len(rom.languages) > 0:
SubElement(game, "lang").text = rom.languages[0]
@@ -85,6 +90,45 @@ class GamelistExporter:
if rom.gamelist_id:
SubElement(game, "id").text = str(rom.gamelist_id)
# Provider specific metadata
if rom.ss_metadata:
if rom.ss_metadata.get("box3d"):
SubElement(game, "box3d").text = rom.ss_metadata["box3d"]
if rom.ss_metadata.get("box2d_back"):
SubElement(game, "backcover").text = rom.ss_metadata["box2d_back"]
if rom.ss_metadata.get("fanart"):
SubElement(game, "fanart").text = rom.ss_metadata["fanart"]
if rom.ss_metadata.get("marquee"):
SubElement(game, "marquee").text = rom.ss_metadata["marquee"]
if rom.ss_metadata.get("miximage"):
SubElement(game, "miximage").text = rom.ss_metadata["miximage"]
if rom.ss_metadata.get("physical"):
SubElement(game, "physicalmedia").text = rom.ss_metadata["physical"]
if rom.ss_metadata.get("title_screen"):
SubElement(game, "title_screen").text = rom.ss_metadata["title_screen"]
if rom.gamelist_metadata:
if rom.gamelist_metadata.get("box3d"):
SubElement(game, "box3d").text = rom.gamelist_metadata["box3d"]
if rom.gamelist_metadata.get("box2d_back"):
SubElement(game, "backcover").text = rom.gamelist_metadata["box2d_back"]
if rom.gamelist_metadata.get("fanart"):
SubElement(game, "fanart").text = rom.gamelist_metadata["fanart"]
if rom.gamelist_metadata.get("marquee"):
SubElement(game, "marquee").text = rom.gamelist_metadata["marquee"]
if rom.gamelist_metadata.get("miximage"):
SubElement(game, "miximage").text = rom.gamelist_metadata["miximage"]
if rom.gamelist_metadata.get("player_count"):
SubElement(game, "players").text = rom.gamelist_metadata["player_count"]
if rom.gamelist_metadata.get("physical"):
SubElement(game, "physicalmedia").text = rom.gamelist_metadata[
"physical"
]
if rom.gamelist_metadata.get("title_screen"):
SubElement(game, "title_screen").text = rom.gamelist_metadata[
"title_screen"
]
# Add scraping info
scrap = SubElement(game, "scrap")
scrap.set("name", "RomM")
@@ -92,14 +136,11 @@ class GamelistExporter:
return game
def export_platform(
self, platform_id: int, rom_ids: Optional[List[int]] = None, request=None
) -> str:
def export_platform_to_xml(self, platform_id: int, request=None) -> str:
"""Export a platform's ROMs to gamelist.xml format
Args:
platform_id: Platform ID to export
rom_ids: Optional list of specific ROM IDs to export
Returns:
XML string in gamelist.xml format
@@ -108,22 +149,14 @@ class GamelistExporter:
if not platform:
raise ValueError(f"Platform with ID {platform_id} not found")
# Get ROMs for the platform
if rom_ids:
roms = db_rom_handler.get_roms_by_ids(rom_ids)
else:
roms = db_rom_handler.get_roms_scalar(platform_id=platform_id)
roms = db_rom_handler.get_roms_scalar(platform_id=platform_id)
# Create root element
root = Element("gameList")
# Platform directory for relative paths
platform_dir = os.path.join(self.base_path, "roms", platform.fs_slug)
# Add games
for rom in roms:
if rom and not rom.missing_from_fs:
game_element = self._create_game_element(rom, platform_dir, request)
game_element = self._create_game_element(rom, request=request)
root.append(game_element)
# Convert to XML string
@@ -133,61 +166,37 @@ class GamelistExporter:
log.info(f"Exported {len(roms)} ROMs for platform {platform.name}")
return xml_str
def export_multiple_platforms(
self, platform_ids: List[int], rom_ids: Optional[List[int]] = None, request=None
) -> Dict[str, str]:
"""Export multiple platforms to separate gamelist.xml files
Args:
platform_ids: List of platform IDs to export
rom_ids: Optional list of specific ROM IDs to export
Returns:
Dictionary mapping platform names to XML strings
"""
results = {}
for platform_id in platform_ids:
try:
platform = db_platform_handler.get_platform(platform_id)
if platform:
xml_content = self.export_platform(platform_id, rom_ids, request)
results[platform.fs_slug] = xml_content
except Exception as e:
log.error(f"Failed to export platform {platform_id}: {e}")
return results
def export_roms_to_file(
async def export_platform_to_file(
self,
platform_id: int,
output_path: str,
rom_ids: Optional[List[int]] = None,
request=None,
) -> bool:
"""Export platform ROMs to a gamelist.xml file
"""Export platform ROMs to gamelist.xml file in the platform's directory
Args:
platform_id: Platform ID to export
output_path: Path where to save the gamelist.xml file
rom_ids: Optional list of specific ROM IDs to export
request: FastAPI request object for URL generation
Returns:
True if successful, False otherwise
"""
try:
xml_content = self.export_platform(platform_id, rom_ids, request)
platform = db_platform_handler.get_platform(platform_id)
if not platform:
log.error(f"Platform with ID {platform_id} not found")
return False
# Ensure output directory exists
os.makedirs(os.path.dirname(output_path), exist_ok=True)
platform_fs_structure = fs_platform_handler.get_plaform_fs_structure(
platform.fs_slug
)
# Write to file
with open(output_path, "w", encoding="utf-8") as f:
f.write(xml_content)
xml_content = self.export_platform_to_xml(platform_id, request=request)
await fs_platform_handler.write_file(
xml_content.encode("utf-8"), platform_fs_structure, "gamelist.xml"
)
log.info(f"Exported gamelist.xml to {output_path}")
log.info(f"Exported gamelist.xml to {platform_fs_structure}/gamelist.xml")
return True
except Exception as e:
log.error(f"Failed to export gamelist.xml to {output_path}: {e}")
log.error(f"Failed to export gamelist.xml for platform {platform_id}: {e}")
return False

View File

@@ -190,7 +190,7 @@ def extract_metadata_from_gamelist_rom(game: Element) -> GamelistMetadata:
class GamelistHandler(MetadataHandler):
"""Handler for EmulationStation gamelist.xml metadata source"""
"""Handler for ES-DE gamelist.xml metadata source"""
@classmethod
def is_enabled(cls) -> bool:
@@ -289,16 +289,16 @@ class GamelistHandler(MetadataHandler):
# Build list of screenshot URLs
url_screenshots = []
if rom_media["title_screen"]:
title_screen_path = fs_platform_handler.validate_path(
f"{platform_dir}/{rom_media['title_screen']}"
)
url_screenshots.append(f"file://{str(title_screen_path)}")
if rom_media["screenshot"]:
screenshot_path = fs_platform_handler.validate_path(
f"{platform_dir}/{rom_media['screenshot']}"
)
url_screenshots.append(f"file://{str(screenshot_path)}")
if rom_media["title_screen"]:
title_screen_path = fs_platform_handler.validate_path(
f"{platform_dir}/{rom_media['title_screen']}"
)
url_screenshots.append(f"file://{str(title_screen_path)}")
if rom_media["miximage"] and get_cover_style() != "miximage":
miximage_path = fs_platform_handler.validate_path(
f"{platform_dir}/{rom_media['miximage']}"

View File

@@ -379,7 +379,7 @@ class Rom(BaseModel):
)
@property
def merged_ra_metadata(self) -> dict[str, list] | None:
def merged_ra_metadata(self) -> dict[str, Any] | None:
if self.ra_metadata and "achievements" in self.ra_metadata:
for achievement in self.ra_metadata.get("achievements", []):
achievement["badge_path_lock"] = (

View File

@@ -52,7 +52,7 @@ filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is
# scan:
# priority:
# metadata: # Top-level metadata source priority
# - "gamelist" # EmulationStation (highest priority)
# - "gamelist" # ES-DE (highest priority)
# - "igdb" # IGDB
# - "moby" # MobyGames
# - "ss" # Screenscraper
@@ -62,7 +62,7 @@ filesystem: {} # { roms_folder: 'roms' } For example if your folder structure is
# - "flashpoint" # Flashpoint Project
# - "hltb" # HowLongToBeat (lowest priority)
# artwork: # Cover art and screenshots
# - "gamelist" # EmulationStation
# - "gamelist" # ES-DE
# - "igdb" # IGDB
# - "moby" # MobyGames
# - "ss" # Screenscraper

View File

@@ -2,6 +2,7 @@
import VuePdfApp from "vue3-pdf-app";
import { useTheme, useDisplay } from "vuetify";
import type { DetailedRom } from "@/stores/roms";
import { FRONTEND_RESOURCES_PATH } from "@/utils";
defineProps<{ rom: DetailedRom }>();
const { xs } = useDisplay();
@@ -100,7 +101,7 @@ const pdfViewerConfig = {
:config="{ toolbar: false }"
:theme="theme.name.value == 'dark' ? 'dark' : 'light'"
style="height: 100dvh"
:pdf="`/assets/romm/resources/${rom.path_manual}`"
:pdf="`${FRONTEND_RESOURCES_PATH}/${rom.path_manual}`"
/>
</template>

View File

@@ -8,7 +8,6 @@ import { useDisplay } from "vuetify";
import DeletePlatformDialog from "@/components/common/Platform/Dialog/DeletePlatform.vue";
import PlatformIcon from "@/components/common/Platform/PlatformIcon.vue";
import RSection from "@/components/common/RSection.vue";
import gamelistApi from "@/services/api/gamelist";
import platformApi from "@/services/api/platform";
import socket from "@/services/socket";
import storeAuth from "@/stores/auth";
@@ -165,40 +164,6 @@ async function setAspectRatio() {
}
}
async function exportGamelist() {
if (!currentPlatform.value) return;
try {
const blob = await gamelistApi.exportPlatformGamelist({
platformId: currentPlatform.value.id,
});
// Create download link
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `gamelist_${currentPlatform.value.fs_slug}.xml`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
emitter?.emit("snackbarShow", {
msg: `Gamelist exported for ${currentPlatform.value.display_name}`,
icon: "mdi-download",
color: "green",
});
} catch (error: any) {
emitter?.emit("snackbarShow", {
msg: `Failed to export gamelist: ${
error.response?.data?.detail || error.message
}`,
icon: "mdi-close-circle",
color: "red",
});
}
}
watch(
() => currentPlatform.value?.aspect_ratio,
(aspectRatio) => {
@@ -335,16 +300,6 @@ watch(
/>
</template>
</v-btn>
<v-btn
v-if="auth.scopes.includes('roms.read')"
rounded="4"
:tabindex="tabIndex"
class="ml-2 my-1 bg-toplayer"
@click="exportGamelist"
>
<v-icon class="text-romm-blue mr-2"> mdi-download </v-icon>
{{ t("platform.export") }} gamelist.xml
</v-btn>
</div>
</div>
<v-row

View File

@@ -4,86 +4,14 @@ export const gamelistApi = api;
async function exportGamelist({
platformIds,
romIds,
}: {
platformIds: number[];
romIds?: number[];
}): Promise<Blob> {
}): Promise<void> {
const params = new URLSearchParams();
platformIds.forEach((id) => params.append("platform_ids", id.toString()));
if (romIds) {
romIds.forEach((id) => params.append("rom_ids", id.toString()));
}
const response = await api.post(
`/v1/gamelist/export?${params.toString()}`,
{},
{
responseType: "blob",
},
);
return response.data;
}
async function exportPlatformGamelist({
platformId,
romIds,
}: {
platformId: number;
romIds?: number[];
}): Promise<Blob> {
const params = new URLSearchParams();
if (romIds) {
romIds.forEach((id) => params.append("rom_ids", id.toString()));
}
const response = await api.post(
`/v1/gamelist/export/platform/${platformId}?${params.toString()}`,
{},
{
responseType: "blob",
},
);
return response.data;
}
async function previewPlatformGamelist({
platformId,
romIds,
}: {
platformId: number;
romIds?: number[];
}): Promise<{
platform: {
id: number;
name: string;
fs_slug: string;
};
rom_count: number;
roms: Array<{
id: number;
name: string;
fs_name: string;
has_cover: boolean;
has_screenshots: boolean;
has_summary: boolean;
}>;
}> {
const params = new URLSearchParams();
if (romIds) {
romIds.forEach((id) => params.append("rom_ids", id.toString()));
}
const response = await api.get(
`/v1/gamelist/export/platform/${platformId}/preview?${params.toString()}`,
);
return response.data;
await api.post(`/gamelist/export?${params.toString()}`);
}
export default {
exportGamelist,
exportPlatformGamelist,
previewPlatformGamelist,
};

View File

@@ -744,3 +744,5 @@ export function platformCategoryToIcon(category: string) {
return "";
}
}
export const FRONTEND_RESOURCES_PATH = "/assets/romm/resources";